Responses and routing

Return results from the driver to the right sequence, match them to their requests, and avoid the bugs that come from reusing objects.

Lesson
U4.2
Time
25 min
Runs on
Verilator with UVM
Check
5 questions

A sequence sends four values, 10, 20, 30 and 40, then collects the four results. The design answers 11, 21, 31 and 41 on the bus, yet the sequence logs 41 four times. The driver created one response object at the start and refilled it for every item, so all four queued responses were the same object.

What the bus carried and what the sequence logged
01234stepitem 1item 2item 3item 4collectx10203040-y on the bus11213141-response objectA, y=11A, y=21A, y=31A, y=41logged-41 41 41 41four handles, one object

Every queued response pointed at the same object A, so when the sequence read them, all four showed its last value.

Two ways back to the sequence

Many protocols need an answer: read data, an error flag, a status. In U4.1 the driver received the very object the sequence created. The simplest way back uses that: the driver writes the result into req and calls item_done(), and the sequence reads the field once finish_item returns.

The second way is a separate response object. The driver creates it, fills it in, and returns it with item_done(rsp) or seq_item_port.put_response(rsp). The sequence picks it up with get_response(rsp), which blocks until a response for this sequence arrives.

What you needWrite into reqSeparate response
One result per item, ready at item_doneSimplest choiceWorks too
A result that arrives after item_done, as on a pipelined busToo late: the sequence has moved onSend it later with put_response
Several results for one requestOnly one place to writeOne response object each
A result of a different type than the requestNot possibleSet the driver's second type parameter, RSP

When each way of returning results fits.

The rest of this lesson is about separate responses, because they need routing.

How a response finds its sequence

Several sequences can share one sequencer, so a response needs an address, and every item carries two numbers for this. When finish_item sends an item, the sequencer stamps it with the sending sequence's sequence ID. The sequence also numbers its items with a transaction ID: 1, 2, 3 and so on, unless you set one yourself.

A new response object has neither. rsp.set_id_info(req) copies both numbers from the request into the response. When the response comes back, the sequencer reads its sequence ID, finds that sequence and adds the response to the sequence's response queue. get_response takes responses from that queue.

IDSet byUsed for
Sequence IDThe sequencer, when the item is sentChoosing which sequence's queue gets the response
Transaction IDThe sequence, numbering its items from 1Matching a response to one request inside that sequence

The two numbers that set_id_info copies from request to response.

  1. The request goes out

    The sequence calls finish_item. The sequencer stamps the item with the sequence ID, and the sequence gives it the next transaction ID.

  2. The driver builds a response

    It creates a new object, calls rsp.set_id_info(req) and fills in the result fields.

  3. The response comes back

    The driver calls item_done(rsp), or put_response(rsp) at any later time. Both hand the response to the sequencer.

  4. The sequencer routes it

    It looks up the sequence by sequence ID and adds the response to that sequence's queue.

  5. The sequence collects it

    Its get_response call returns the response, either the oldest one or the one with the transaction ID it asked for.

Forget set_id_info and the response has no sequence ID. The sequencer cannot route it, and uvm-core stops the test with a fatal error: "Driver put a response with null sequence_id".

These item classes use the default copy, so req.clone() leaves their payload and routing IDs at defaults. A custom do_copy can copy both deliberately. Set the response fields and call set_id_info(req) to make the reply's contents and destination explicit.

Two sequences, one driver

The example runs two sequences in parallel on one sequencer. low sends 10 and 11, and high sends 100 and 101. The sequencer grants them in turn, so the driver sees 10, 100, 11, 101.

Both sequences number their items from 1, so the bus carries two items with transaction ID 1, and that is fine. The sequence ID keeps the two streams apart, and a transaction ID only has to be unique within one sequence.

Each sequence asks for its own answer with get_response(rsp, it.get_transaction_id()). With an ID, get_response waits for that response and leaves others queued. Without one, it takes the oldest arrival. That also supports reordered results if the collector uses rsp.get_transaction_id() to find the matching request, as U6.1 demonstrates.

Read the example's log by time. Each item takes two clock cycles, so responses arrive at 20, 40, 60 and 80 ns, alternating between low and high. Neither sequence ever prints the other's answer, although both wait on the same driver and use the same transaction IDs.

Two sequences, one driver, responses by sequence IDSolid tokens are requests and responses; each label names the sequence it belongs to and its transaction ID. Times are the example's response lines. The driver calls item_done(rsp) two clock cycles after it takes each item.
Two sequences, one driver, responses by sequence IDSolid tokens are requests and responses; each label names the sequence it belongs to and its transaction ID. Times are the example's response lines. The driver calls `item_done(rsp)` two clock cycles after it takes each item. sequence low Sends 10 and 11;get_response by tid sequence high Sends 100 and 101;get_response by tid component Sequencer Stamps the sequenceID; routes responses component Driver New rsp;set_id_info(req);item_done(rsp) finish_item(it)finish_item(it)get_next_item /item_done(rsp)low's queuehigh's queue
  1. Both ask; low goes first

    Both sequences call start_item as the test forks them. The sequencer grants low, stamps its item with low's sequence ID, and the driver takes it.

    More context

    The transaction ID, 1, comes from the sequence's own count. The sequence ID comes from the sequencer when the item is sent.

    Follow the calls, values and waits
    1. data low tid=1: x=10: low → Sequencer
    2. data low tid=1: x=10: Sequencer → Driver
    3. Waiting: low — in finish_item
    4. Waiting: high — waiting for a grant
  2. A response addressed to low20 ns (illustrative)

    The driver builds a new response, copies both IDs with set_id_info(req) and calls item_done(rsp). The sequencer reads the sequence ID and queues it for low.

    More context

    Log line 20 ns low tid=1 x=10 y=11. get_response(rsp, 1) returns at once, because the response is already in low's queue.

    Follow the calls, values and waits
    1. data rsp for low tid=1: y=11: Driver → Sequencer
    2. data rsp for low tid=1: y=11: Sequencer → low
    3. Released: low
  3. high is granted next20 ns (illustrative)

    The driver asks again in the same time step. high has waited longest, so it is granted; its item also carries transaction ID 1.

    More context

    Two items with transaction ID 1 are now in the system. They differ only in their sequence IDs, and that is enough.

    Follow the calls, values and waits
    1. data high tid=1: x=100: high → Sequencer
    2. data high tid=1: x=100: Sequencer → Driver
    3. Waiting: high — in finish_item
    4. Waiting: low — waiting for a grant
    5. Released: high
  4. tid=1 again, but for high40 ns (illustrative)

    high's response is routed to high's queue. The driver then takes low's second item.

    More context

    Log line 40 ns high tid=1 x=100 y=101. low never sees this response, although it has the same transaction ID as low's first.

    Follow the calls, values and waits
    1. data rsp for high tid=1: y=101: Driver → Sequencer
    2. data rsp for high tid=1: y=101: Sequencer → high
    3. data low tid=2: x=11: low → Sequencer
    4. data low tid=2: x=11: Sequencer → Driver
    5. Waiting: low — in finish_item
    6. Waiting: high — waiting for a grant
    7. Released: high
    8. Released: low
  5. low's second answer60 ns (illustrative)

    low's tid=2 response reaches low, and high's second item goes out.

    More context

    Log line 60 ns low tid=2 x=11 y=12. low has sent both its values and its body ends.

    Follow the calls, values and waits
    1. data rsp for low tid=2: y=12: Driver → Sequencer
    2. data rsp for low tid=2: y=12: Sequencer → low
    3. data high tid=2: x=101: high → Sequencer
    4. data high tid=2: x=101: Sequencer → Driver
    5. Waiting: high — in finish_item
    6. Released: low
    7. Released: high
  6. high's second answer80 ns (illustrative)

    The last response reaches high. Each sequence printed only its own two answers.

    More context

    Log line 80 ns high tid=2 x=101 y=102. Without set_id_info, the first item_done(rsp) at 20 ns would have stopped the test with a fatal error.

    Follow the calls, values and waits
    1. data rsp for high tid=2: y=102: Driver → Sequencer
    2. data rsp for high tid=2: y=102: Sequencer → high
    3. Released: high
Read the diagram as text
  • low: Sends 10 and 11; get_response by tid
  • high: Sends 100 and 101; get_response by tid
  • Sequencer: Stamps the sequence ID; routes responses
  • Driver: New rsp; set_id_info(req); item_done(rsp)
  • low → Sequencer: finish_item(it)
  • high → Sequencer: finish_item(it)
  • Driver → Sequencer: get_next_item / item_done(rsp)
  • Sequencer → low: low's queue
  • Sequencer → high: high's queue

The sequence ID picks the queue; the transaction ID picks the response inside it.

Collect every response

A response queue holds 8 responses by default. A sequence that receives responses but never calls get_response fills it after 8 items, and later responses are thrown away.

Three settings change this: set_response_queue_depth(-1) makes the queue unbounded, and set_response_queue_error_report_enabled(1) reports each dropped response as an error. use_response_handler(1) makes the sequencer call the sequence's response_handler function for each response instead of queuing it.

The simplest rule is to match the two sides. If a sequence wants responses, it reads every one. If it does not, the driver should not send any. The usual way into this trap is a driver written for one sequence that reads responses, later reused with another that does not. The first 8 items behave, and everything after them is lost.

Reusing an item after you send it

The hook's bug has a twin on the sequence side. A sequence that creates one item before its loop sends the same object every time. Anything that kept a handle to an earlier send, such as a list of sent items or a pipelined driver, now sees the newest values instead.

There is a quieter effect too: the sequence numbers an item only while its transaction ID is still unset. A reused item keeps the ID from its first send, so every send carries transaction ID 1, and get_response by ID can no longer tell them apart.

One object, sent three times

incr_item it = incr_item::type_id::create("it");
repeat (3) begin
  start_item(it);
  it.x = next_x++;
  finish_item(it);
  sent.push_back(it);     // three handles, one object
end

All three entries show the last value, and all three sends carry transaction ID 1.

A new object for every send

repeat (3) begin
  incr_item it = incr_item::type_id::create("it");
  start_item(it);
  it.x = next_x++;
  finish_item(it);
  sent.push_back(it);
end

Each entry is its own object, numbered 1, 2 and 3.

The rule is the same on both sides. Create a new request for every send and a new response for every reply. If you need to keep an item and also change it afterwards, keep a clone.

Try it

Two sequences share the driver. Each line is printed by the sequence that received the response, with the transaction ID it matched on. Both sequences use transaction IDs 1 and 2, and each still gets only its own answers.

Delete the rsp.set_id_info(req); line in the driver and run again: the first item_done stops the test with a fatal error. Then restore it and make low send three values to see transaction ID 3.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: 20 ns low tid=1 x=10 y=11 40 ns high tid=1 x=100 y=101 60 ns low tid=2 x=11 y=12 80 ns high tid=2 x=101 y=102 UVM_ERROR : 0

Check yourself

Predict the outputWhat line does the $display print? UVM's own messages are not part of the answer.
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;
class item_c extends uvm_sequence_item;
  `uvm_object_utils(item_c)
  bit [7:0] x;
  function new(string name = "item_c"); super.new(name); endfunction
endclass
class drv_c extends uvm_driver #(item_c);
  `uvm_component_utils(drv_c)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  task run_phase(uvm_phase phase);
    forever begin
      seq_item_port.get_next_item(req);
      #10;
      seq_item_port.item_done();
    end
  endtask
endclass
class seq_c extends uvm_sequence #(item_c);
  `uvm_object_utils(seq_c)
  function new(string name = "seq_c"); super.new(name); endfunction
  task body();
    item_c it = item_c::type_id::create("it");
    item_c sent[$];
    for (int i = 1; i <= 3; i++) begin
      start_item(it);
      it.x = 10 * i;
      finish_item(it);
      sent.push_back(it);
    end
    $display("ids %0d %0d %0d, values %0d %0d %0d",
             sent[0].get_transaction_id(), sent[1].get_transaction_id(), sent[2].get_transaction_id(),
             sent[0].x, sent[1].x, sent[2].x);
  endtask
endclass
class my_test extends uvm_test;
  `uvm_component_utils(my_test)
  drv_c drv;
  uvm_sequencer #(item_c) sqr;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    drv = drv_c::type_id::create("drv", this);
    sqr = uvm_sequencer #(item_c)::type_id::create("sqr", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    drv.seq_item_port.connect(sqr.seq_item_export);
  endfunction
  task run_phase(uvm_phase phase);
    seq_c seq = seq_c::type_id::create("seq");
    phase.raise_objection(this);
    seq.start(sqr);
    phase.drop_objection(this);
  endtask
endclass
module tb;
  initial run_test("my_test");
endmodule
Spot the bugThe sequence sends 10, 20 and 30 and matches each response by transaction ID. Each option is the rest of the driver loop, after get_next_item(req) and a #10 delay. Which one fails?
ConceptSequences low and high share a sequencer, and each has just sent its first item. Both requests carry transaction ID 1. How does the sequencer deliver each response to the right sequence?
ConceptA sequence sends 10 items, and the driver answers each with item_done(rsp), but the sequence never calls get_response. What happens to responses 9 and 10 in uvm-core 2020.3.1?
ConceptA bus returns read data two cycles after the driver has accepted the next request, so the driver calls item_done before the data arrives. How should it return the data?

Remember

  • A driver can return a result by writing into req before item_done, or in a separate response object.
  • set_id_info(req) copies the sequence ID and transaction ID; without them a response cannot be routed.
  • The sequence ID picks the sequence; the transaction ID matches a response to one request within it.
  • Read every response: the queue holds 8 by default, and uvm-core 2020.3.1 drops the overflow silently.
  • Create a new object for every request and every response; a reused object changes under anyone holding it.