A new AXI read driver lets four reads be outstanding, as the protocol allows. The first test passes. The second, with random slave latencies, fails with data mismatches on reads that the waveform shows returning the correct data. The sequence is pairing each response with the wrong request: it assumed responses come back in the order the requests went out.
The bench's APB driver never has this problem, because it answers inside the request object before item_done. This lesson is about everything else: responses that come later, responses that come out of order, and the queue that holds them.
You hand in a shirt and get a ticket; you do not stand at the counter until it is clean. Later you come back and collect your shirt by ticket number, even if other people's were ready first. A pipelined driver works the same way. It takes the request and releases the sequence at once (the ticket), drives it, and much later sends a response that carries the request's IDs. The sequence collects responses from its response queue, either the next one or the one matching a given transaction ID.
So a sequence has two separate channels with the driver. The request handshake (finish_item waits for item_done) says "the driver has taken this". The response path (put_response to get_response) says "here is the result". For the bench's APB driver they coincide; for a pipelined driver they are far apart.
Every sequence owns a response queue. The sequencer routes each response to the sequence named in the response's sequence ID, and pushes it onto that queue, which holds 8 by default. get_response(rsp) pops the oldest; get_response(rsp, id) waits for the one with that transaction ID. If the sequence never collects, the 9th response is dropped, and in UVM 1.2 each drop is a UVM_ERROR ("Response queue overflow, response was dropped"). Three knobs control it:
Call | Effect |
|---|---|
| Change the depth; -1 makes it unbounded (and makes a leak grow forever instead of failing) |
| Drop silently instead of erroring (UVM 1.2 name; still present in 1800.2) |
| The IEEE 1800.2 name for the opposite switch |
item_done(rsp) is item_done() followed by put_response(rsp): it completes the request and queues a response in one call. It suits an in-order driver whose response is a separate object. seq_item_port.put_response(rsp) queues a response without touching the request handshake, for drivers that completed the request earlier, typically with get(). Either way the response must carry the request's IDs, set with `rsp.set_id_info(req)`, which copies the sequence ID and transaction ID. A response with no sequence ID is a SQRPUT fatal ("Driver put a response with null sequence_id"); one for a sequence that has already ended is dropped with a warning ("sequence not found").
This driver allows four outstanding reads. get() takes each request and releases the sequence immediately; a forked thread answers after a latency that depends on the address, so later requests can finish first:
// snippet: seq_lab_tb.sv (lab_driver supplies vif and mem_rd())
class lab_pipe_driver extends lab_driver;
`uvm_component_utils(lab_pipe_driver)
semaphore slots = new(4); // at most 4 outstanding
function new(string name, uvm_component parent); super.new(name, parent); endfunction
task run_phase(uvm_phase phase);
wait (vif.rst_n === 1'b1);
forever begin
lab_item r;
seq_item_port.get(r); // get() = get_next_item() + item_done()
slots.get(1);
fork
automatic lab_item t = r;
respond(t);
join_none
end
endtask
task respond(lab_item t);
lab_item rsp;
repeat (2 + 3 * (3 - t.addr[3:2])) @(posedge vif.clk); // higher addresses answer sooner
rsp = lab_item::type_id::create("rsp");
rsp.set_id_info(t); // route back to t's sequence, with t's transaction ID
rsp.addr = t.addr;
rsp.rdata = mem_rd(t.addr);
`uvm_info("PIPE", $sformatf("response id=%0d addr=%02h", rsp.get_transaction_id(), rsp.addr), UVM_LOW)
seq_item_port.put_response(rsp);
slots.put(1);
endtask
endclassThe automatic copy declared in the fork gives each forked thread its own handle, taken when the fork starts; it is the standard idiom for fork ... join_none inside a loop. The response is a new object, so the request the sequence holds is never changed behind its back.
// snippet: seq_lab_tb.sv
task body();
int ids[$];
for (int i = 0; i < 4; i++) begin
lab_item it = lab_item::type_id::create($sformatf("rd%0d", i));
start_item(it);
if (!it.randomize() with { write == 0; addr == 8'(4 * i); }) `uvm_fatal("RAND", "randomize failed")
finish_item(it); // returns at once: get() already did item_done
`uvm_info("SEQ", $sformatf("sent id=%0d addr=%02h", it.get_transaction_id(), it.addr), UVM_LOW)
ids.push_back(it.get_transaction_id());
end
foreach (ids[k]) begin
lab_item rsp;
get_response(rsp, ids[k]); // wait for THIS id, whatever order they arrive in
`uvm_info("SEQ", $sformatf("got id=%0d rdata=%08h", rsp.get_transaction_id(), rsp.rdata), UVM_LOW)
end
endtaskThe transaction ID is assigned by the sequence when the item is sent, so read it after finish_item. And because finish_item returned before the driver did anything, the request's own rdata is meaningless here: the answer is only in rsp.
If the sequence does not want to block, use_response_handler(1) sends every response to the virtual function response_handler() instead of the queue. The queue can then never overflow, but the handler is a function, so it cannot wait; it records and returns:
// snippet: seq_lab_tb.sv
function void response_handler(uvm_sequence_item response);
lab_item rsp;
$cast(rsp, response);
got++;
`uvm_info("HANDLER", $sformatf("id=%0d rdata=%08h", rsp.get_transaction_id(), rsp.rdata), UVM_LOW)
endfunction
task body();
use_response_handler(1);
// ... send four reads as above ...
wait (got == 4);
endtaskThe pipelined sequence: all four reads go out at 22 ns, the responses come back in reverse order, and get_response still hands them over in request order:
UVM_INFO seq_lab_tb.sv(235) @ 22000: uvm_test_top.env.sqr@@pipe [SEQ] sent id=1 addr=00
UVM_INFO seq_lab_tb.sv(235) @ 22000: uvm_test_top.env.sqr@@pipe [SEQ] sent id=2 addr=04
UVM_INFO seq_lab_tb.sv(235) @ 22000: uvm_test_top.env.sqr@@pipe [SEQ] sent id=3 addr=08
UVM_INFO seq_lab_tb.sv(235) @ 22000: uvm_test_top.env.sqr@@pipe [SEQ] sent id=4 addr=0c
UVM_INFO seq_lab_tb.sv(96) @ 35000: uvm_test_top.env.drv [PIPE] response id=4 addr=0c
UVM_INFO seq_lab_tb.sv(96) @ 65000: uvm_test_top.env.drv [PIPE] response id=3 addr=08
UVM_INFO seq_lab_tb.sv(96) @ 95000: uvm_test_top.env.drv [PIPE] response id=2 addr=04
UVM_INFO seq_lab_tb.sv(96) @ 125000: uvm_test_top.env.drv [PIPE] response id=1 addr=00
UVM_INFO seq_lab_tb.sv(241) @ 125000: uvm_test_top.env.sqr@@pipe [SEQ] got id=1 rdata=a0000000
UVM_INFO seq_lab_tb.sv(241) @ 125000: uvm_test_top.env.sqr@@pipe [SEQ] got id=2 rdata=a0000004
UVM_INFO seq_lab_tb.sv(241) @ 125000: uvm_test_top.env.sqr@@pipe [SEQ] got id=3 rdata=a0000008
UVM_INFO seq_lab_tb.sv(241) @ 125000: uvm_test_top.env.sqr@@pipe [SEQ] got id=4 rdata=a000000cWith the handler version, each response is processed the moment it arrives, in arrival order:
UVM_INFO seq_lab_tb.sv(96) @ 35000: uvm_test_top.env.drv [PIPE] response id=4 addr=0c
UVM_INFO seq_lab_tb.sv(254) @ 35000: uvm_test_top.env.sqr@@hseq [HANDLER] id=4 rdata=a000000c
UVM_INFO seq_lab_tb.sv(96) @ 65000: uvm_test_top.env.drv [PIPE] response id=3 addr=08
UVM_INFO seq_lab_tb.sv(254) @ 65000: uvm_test_top.env.sqr@@hseq [HANDLER] id=3 rdata=a0000008A driver switched from item_done() to item_done(rsp) with a fresh response object, and a polling sequence sent ten reads without ever calling get_response. With the error report on, as UVM 1.2 has it by default, the 9th and 10th responses are dropped loudly:
UVM_ERROR @ 195000: uvm_test_top.env.sqr@@flood [uvm_test_top.env.sqr.flood] Response queue overflow, response was dropped
UVM_ERROR @ 215000: uvm_test_top.env.sqr@@flood [uvm_test_top.env.sqr.flood] Response queue overflow, response was dropped
UVM_INFO seq_lab_tb.sv(212) @ 215000: uvm_test_top.env.sqr@@flood [FLOOD] 10 reads done, 8 responses waiting, depth 8Now the uncomfortable part. Run the same sequence on the IEEE 1800.2-2020 library used for this course's benches, without touching any knob, and the log is clean: 10 reads done, 8 responses waiting, depth 8 and UVM_ERROR : 0. In that library the report flag starts disabled, so the two responses vanish without a word. The two lines above come from the same run with set_response_queue_error_report_disabled(0) called first. Do not rely on the error to find this bug; rely on the rule instead: a sequence collects every response it causes, or the driver does not send one.
item_done(rsp) where rsp is created with type_id::create but set_id_info is never called. What happens on the first response?The response's sequence ID is still -1, so the sequencer cannot tell which sequence it belongs to and stops with a SQRPUT fatal: "Driver put a response with null sequence_id". set_id_info(req) is what makes routing possible.
get_response(rsp) (no ID) four times instead of by ID. What does it get?The responses in arrival order: ID 4, 3, 2, 1 in the lab run. That is fine if the sequence matches each one to its request by the ID it carries, and wrong if it assumes the k-th response belongs to the k-th request, which is exactly the opening bug.
When the driver puts them, the sequencer can no longer find the sequence and drops each one with a warning ("Dropping response for sequence N, sequence not found"). A sequence that causes responses must wait for all of them before its body returns.
Next: sometimes a sequence must end before its work is done, because the DUT went into reset. Stopping sequences and reset shows how to stop one cleanly, and the one call order that ends in a fatal.