A UVM driver can get items from the sequencer in two styles. The most common one uses get_next_item() and item_done(), as described in Using get_next_item(). The other style uses get() to receive the next item and put() to send a response item back to the sequence.
This style is useful when the sequence should continue as soon as the driver has taken an item, and collect the result later, for example the read data returned by the DUT.
8 min read | Intermediate Level
What You'll Learn
- How
get()differs fromget_next_item()in a driver - How the driver sends a response with
put(), and how the sequence receives it withget_response() - When
finish_item()returns in this style - How to read the log of a complete get/put example
How get() and put() Work
The handshake starts the same way as with get_next_item(). The difference is in when the item is marked as done:
- The driver calls
get()and waits for an item. - The sequence calls
start_item(), randomizes the item and callsfinish_item(). get()returns the item and immediately marks it as done. Sofinish_item()returns before the driver has driven anything.- The sequence calls
get_response()and waits. - The driver drives the item to the DUT and then calls
put()with a response. This unblocksget_response().
| get_next_item() style | get() style | |
|---|---|---|
| Driver gets an item with | get_next_item() | get() |
| Item marked as done | When the driver calls item_done() | As soon as get() returns |
finish_item() returns | After the item has been driven | Before the item is driven |
| Sequence knows the item has been driven | When finish_item() returns | When get_response() returns |
| Response | Optional, with item_done(rsp) | Sent with put(rsp) |
What is the Driver Code for get() and put() Calls?
The driver gets an item into the built-in handle req, drives it, stores the returned read data in it, and sends it back with put().
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. Get an item from the sequencer using "get" method
seq_item_port.get(req);
// 2. For simplicity, lets assume the driver drives the item and consumes 20ns of simulation time
#20;
// 3. After the driver is done, assume it gets back a read data called 8'hAA from the DUT
// Assign the read data into the "request" sequence_item object
req.data = 8'hAA;
// 4. Call the "put" method to send the request item back to the sequencer
seq_item_port.put(req);
endtask
endclass
put() does not block. A response must carry the sequence ID and transaction ID of its request, so that the sequencer can route it to the right sequence. Here the response is the request object itself, so the IDs are already correct.
How Does the Sequence Start and Stop an Item?
The sequence uses start_item() and finish_item() as usual. Because finish_item() returns as soon as the driver has taken the item, the sequence then calls get_response() to wait until the driver has finished.
class my_sequence extends uvm_sequence #(my_data);
`uvm_object_utils (my_sequence)
function new (string name = "my_sequence");
super.new (name);
endfunction
// Create a sequence item object handle to store the sequence_item contents
my_data tx;
virtual task body();
// 1. Create the sequence item using standard factory calls
tx = my_data::type_id::create("tx");
// 2. Start this item on the current sequencer
start_item(tx);
// 3. Do late randomization since the class handle pointers are the same
tx.randomize();
// 4. Finish executing the item from the sequence perspective
// The driver could still be actively driving and waiting for response
finish_item(tx);
// 5. Because "finish_item" does not indicate that the driver has finished driving the item,
// the sequence has to wait until the driver explicitly tells the sequencer that the item is over
// So, wait unitl the sequence gets a response back from the sequencer.
get_response(tx);
`uvm_info ("SEQ", $sformatf("get_response() fn call done rsp addr=0x%0h data=0x%0h, exit seq", tx.addr, tx.data), UVM_MEDIUM)
endtask
endclass
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. A #20 delay stands in for driving the DUT, and the value 8'hAA stands in for the read data returned by the DUT.
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. Get sequence item from the sequencer using "get" method.
// "req" is a pre-defined variable that comes with the class "uvm_driver"
`uvm_info ("DRIVER", $sformatf ("Waiting for data from sequencer"), UVM_MEDIUM)
seq_item_port.get(req);
// 2. Assume that the driver drives this data during this time. For our purpose let's consider that the driver
// consumes 20ns of simulation time
`uvm_info ("DRIVER", $sformatf ("Start driving tx addr=0x%0h data=0x%0h", req.addr, req.data), UVM_MEDIUM)
#20;
// 3. Lets also assume that the DUT returned some read data back to the driver, which needs to be sent back
// to the sequence. Note that read data is put into the same "request" class object handle
`uvm_info ("DRIVER", $sformatf ("#20 delay over, curr data=0x%0h", req.data), UVM_MEDIUM)
req.data = 8'hAA;
// 4. The driver calls the "put" method and sends back the response data to the sequencer
`uvm_info ("DRIVER", $sformatf ("About to call put() with new data=0x%0h", req.data), UVM_MEDIUM)
seq_item_port.put(req);
`uvm_info ("DRIVER", $sformatf ("Finish driving tx addr=0x%0h data=0x%0h", req.addr, req.data), UVM_MEDIUM)
endtask
endclass
Define the sequence
The sequence prints a message after each step, so that the log shows the order of events.
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. Note that this sequence has an internal
// variable called "req" which can be directly used as well instead of "tx"
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, wait for rsp"), UVM_MEDIUM)
// 5. Wait for the driver to finish driving the object, and respond back with a response object
// The transaction is said to be complete at this point. This task is blocking in nature and hence
// the sequence will end only after the driver returns a response item.
get_response(tx);
`uvm_info ("SEQ", $sformatf("get_response() fn call done rsp addr=0x%0h data=0x%0h, exit seq", tx.addr, tx.data), 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(76) @ 0: uvm_test_top.m_top_env.m_seqr0@@m_seq [SEQ] About to call start_item UVM_INFO testbench.sv(78) @ 0: uvm_test_top.m_top_env.m_seqr0@@m_seq [SEQ] start_item() fn call done UVM_INFO testbench.sv(80) @ 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(82) @ 0: uvm_test_top.m_top_env.m_seqr0@@m_seq [SEQ] finish_item() fn call done, wait for rsp UVM_INFO testbench.sv(37) @ 20: uvm_test_top.m_top_env.m_drv0 [DRIVER] #20 delay over, curr data=0x1d UVM_INFO testbench.sv(39) @ 20: uvm_test_top.m_top_env.m_drv0 [DRIVER] About to call put() with new data=0xaa UVM_INFO testbench.sv(41) @ 20: uvm_test_top.m_top_env.m_drv0 [DRIVER] Finish driving tx addr=0x8f data=0xaa UVM_INFO testbench.sv(84) @ 20: uvm_test_top.m_top_env.m_seqr0@@m_seq [SEQ] get_response() fn call done rsp addr=0x8f data=0xaa, exit seq 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, 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(), the sequence is granted, randomizes the item and callsfinish_item(). - Still at time 0:
finish_item()returns, even though the driver has only just started driving. The sequence then waits inget_response(). - At time 20: the driver sets data to
0xaaand callsput().get_response()returns, and the sequence printsdata=0xaa.
The sequence sees the new data because the response is the same object as the request. tx in the sequence and req in the driver point to one object, so the change made by the driver is visible in both.
Quick Check
Q: The driver's #20 delay is changed to #50. At what times are the messages finish_item() fn call done and get_response() fn call done printed?
Show Answer
finish_item() still returns at time 0, because get() marks the item as done right away. get_response() returns at time 50, when the driver calls put().
Where is This Used?
Pipelined protocols such as AXI can have several transfers in progress at once. A driver for such a protocol takes new items with get() while earlier ones are still on the bus, and sends a response with put() as each transfer completes. The sequence matches responses to requests through their transaction IDs.
Common Mistakes
Mistake #1: Assuming finish_item() means the item was driven
With get(), finish_item() returns before the driver starts driving. A sequence that needs the result, such as read data, must call get_response() and use the values from the response.
Mistake #2: Sending a new response object without set_id_info()
If the driver creates a separate response object, it must call rsp.set_id_info(req) before put(rsp). Without the IDs, the sequencer does not know which sequence the response belongs to, and reports an error.
Mistake #3: Never calling get_response()
Each response is stored in a queue inside the sequence until get_response() collects it. The queue holds 8 responses by default. If the sequence never collects them, the queue overflows, later responses are dropped, and an error is reported.
Practice Exercise
Challenge: Use a Separate Response Object
Change the driver so that it does not modify req. Instead, it creates a new response object in the built-in handle rsp, copies the address, sets the read data to 8'hAA and sends it with put(). What value does tx.data have in the sequence after get_response()?
Hint
The response needs the IDs of the request. Look at the second common mistake.
Solution
// Driver
seq_item_port.get (req);
#20;
rsp = my_data::type_id::create ("rsp");
rsp.set_id_info (req); // Same sequence ID and transaction ID as req
rsp.addr = req.addr;
rsp.data = 8'hAA;
seq_item_port.put (rsp);
// Sequence
finish_item (tx);
get_response (rsp); // Built-in rsp handle of the sequence
`uvm_info ("SEQ", $sformatf ("tx.data=0x%0h rsp.data=0x%0h", tx.data, rsp.data), UVM_MEDIUM)
tx.data keeps its randomized value, 0x1d in the log. Only the response object holds 0xaa, because the driver no longer changes the request.
Summary
In this tutorial, you learned:
get()returns the next item and marks it as done at once, sofinish_item()returns before the item is driven- The driver sends a response with
put(), which does not block - The sequence waits for the response with
get_response() - A response must carry the IDs of its request, set with
set_id_info()when a new object is used - Responses that are never collected fill the sequence's response queue