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.
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 need | Write into req | Separate response |
|---|---|---|
One result per item, ready at item_done | Simplest choice | Works too |
A result that arrives after item_done, as on a pipelined bus | Too late: the sequence has moved on | Send it later with put_response |
| Several results for one request | Only one place to write | One response object each |
| A result of a different type than the request | Not possible | Set 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.
| ID | Set by | Used for |
|---|---|---|
| Sequence ID | The sequencer, when the item is sent | Choosing which sequence's queue gets the response |
| Transaction ID | The sequence, numbering its items from 1 | Matching a response to one request inside that sequence |
The two numbers that set_id_info copies from request to response.
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.The driver builds a response
It creates a new object, calls
rsp.set_id_info(req)and fills in the result fields.The response comes back
The driver calls
item_done(rsp), orput_response(rsp)at any later time. Both hand the response to the sequencer.The sequencer routes it
It looks up the sequence by sequence ID and adds the response to that sequence's queue.
The sequence collects it
Its
get_responsecall 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.
item_done(rsp) two clock cycles after it takes each item.- Both ask; low goes first
Both sequences call
start_itemas the test forks them. The sequencer grantslow, 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
- data low tid=1: x=10: low → Sequencer
- data low tid=1: x=10: Sequencer → Driver
- Waiting: low — in finish_item
- Waiting: high — waiting for a grant
- A response addressed to low20 ns (illustrative)
The driver builds a new response, copies both IDs with
set_id_info(req)and callsitem_done(rsp). The sequencer reads the sequence ID and queues it forlow.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
- data rsp for low tid=1: y=11: Driver → Sequencer
- data rsp for low tid=1: y=11: Sequencer → low
- Released: low
- high is granted next20 ns (illustrative)
The driver asks again in the same time step.
highhas 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
- data high tid=1: x=100: high → Sequencer
- data high tid=1: x=100: Sequencer → Driver
- Waiting: high — in finish_item
- Waiting: low — waiting for a grant
- Released: high
- 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
- data rsp for high tid=1: y=101: Driver → Sequencer
- data rsp for high tid=1: y=101: Sequencer → high
- data low tid=2: x=11: low → Sequencer
- data low tid=2: x=11: Sequencer → Driver
- Waiting: low — in finish_item
- Waiting: high — waiting for a grant
- Released: high
- Released: low
- 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
- data rsp for low tid=2: y=12: Driver → Sequencer
- data rsp for low tid=2: y=12: Sequencer → low
- data high tid=2: x=101: high → Sequencer
- data high tid=2: x=101: Sequencer → Driver
- Waiting: high — in finish_item
- Released: low
- Released: high
- 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. Withoutset_id_info, the firstitem_done(rsp)at 20 ns would have stopped the test with a fatal error.Follow the calls, values and waits
- data rsp for high tid=2: y=102: Driver → Sequencer
- data rsp for high tid=2: y=102: Sequencer → high
- 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.
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 : 0Check yourself
Remember
- A driver can return a result by writing into
reqbeforeitem_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.