A UVM sequence response is an item the driver sends back to the sequence that issued a request, usually to return read data or a status. The sequence collects it with get_response(), or receives it automatically in response_handler(). The driver returns it with item_done(rsp), put_response(rsp) or its rsp_port, after copying the request's IDs into it with set_id_info().

Many simple testbenches never use responses, because the driver can write results straight into the request object. Responses become necessary when the result arrives after the driver has already released the request, when one sequence keeps several requests in flight, or when the response is a different type from the request.

8 min read | Intermediate

What You'll Learn

  • Return a response from the driver with the correct sequence and transaction IDs
  • Collect responses in a sequence with get_response()
  • Handle responses asynchronously with response_handler()
  • Avoid response queue overflow and stuck sequences

Syntax


// In the sequence (uvm_sequence #(REQ, RSP), RSP defaults to REQ)
virtual task  get_response(output RSP response, input int transaction_id = -1);
function void use_response_handler(bit enable);
virtual function void response_handler(uvm_sequence_item response);
function void set_response_queue_depth(int value);          // default 8, -1 = unlimited
function void set_response_queue_error_report_disabled(bit value);

// In the driver
rsp.set_id_info(req);                    // copy sequence ID and transaction ID
seq_item_port.item_done(rsp);            // finish the request and return rsp
seq_item_port.put_response(rsp);         // return rsp without finishing anything
rsp_port.write(rsp);                     // via the driver's analysis port

When You Don't Need a Response

With the usual get_next_item() and item_done() handshake, finish_item() in the sequence does not return until the driver calls item_done(). So if the driver writes the result into the request object before calling item_done(), the sequence can read it straight from req:


// Driver
seq_item_port.get_next_item(m_req);
drive_read(m_req);                       // waits for the bus to return data
m_req.rdata = vif.rdata;                 // fill in the result
seq_item_port.item_done();               // no response object

// Sequence
start_item(req);
if (!req.randomize() with { write == 0; addr == 8'h20; }) `uvm_error("SEQ", "Randomization failed")
finish_item(req);                        // returns after item_done()
`uvm_info("SEQ", $sformatf("Read 0x%0h", req.rdata), UVM_MEDIUM)

This is fine for simple, one-at-a-time drivers. It breaks down as soon as the driver calls item_done() before the transfer is really over, which is exactly what a driver that overlaps several transfers does. Then the data has to travel back separately, as a response.

How a Response Finds Its Sequence

Several sequences can share one sequencer, so the sequencer needs to know which sequence a response belongs to. Every request carries a sequence ID and a transaction ID, set when the sequence sends it. rsp.set_id_info(req) copies both into the response. The sequencer then delivers the response to the right sequence, where it waits in that sequence's response queue.

Forget set_id_info() on a freshly created response and the sequencer cannot route it. It rejects the response with a fatal error about a missing sequence ID, and the simulation stops.

Example: Returning Read Data with get_response()

bus_item has random addr, write and data fields, plus rdata and ok fields that only the driver fills in. The driver builds a separate response for every request:


class bus_driver extends uvm_driver #(bus_item);
  `uvm_component_utils(bus_driver)
  // Rest of the code (constructor, build_phase getting vif)

  virtual task run_phase(uvm_phase phase);
    bus_item m_req, m_rsp;
    forever begin
      seq_item_port.get_next_item(m_req);
      drive(m_req);                                 // perform the bus transfer
      $cast(m_rsp, m_req.clone());                  // response starts as a copy
      m_rsp.set_id_info(m_req);                     // route it to the right sequence
      m_rsp.rdata = vif.rdata;
      m_rsp.ok    = !vif.err;
      seq_item_port.item_done(m_rsp);               // finish request, send response
    end
  endtask
endclass

The sequence collects one response for each request it sends:


class read_seq extends uvm_sequence #(bus_item);
  `uvm_object_utils(read_seq)
  // Rest of the code (constructor)

  virtual task body();
    bus_item m_rsp;
    for (int i = 0; i < 4; i++) begin
      req = bus_item::type_id::create("req");
      start_item(req);
      if (!req.randomize() with { write == 0; addr == 8'h20 + i; }) `uvm_error("SEQ", "Randomization failed")
      finish_item(req);
      get_response(m_rsp);                          // blocks until the response arrives
      if (!m_rsp.ok) `uvm_error("SEQ", $sformatf("Read of 0x%0h failed", m_rsp.addr))
      else `uvm_info("SEQ", $sformatf("0x%0h = 0x%0h", m_rsp.addr, m_rsp.rdata), UVM_MEDIUM)
    end
  endtask
endclass

get_response() blocks until a response is in the queue, then returns the oldest one. Because this sequence collects each response straight after its request, the queue never holds more than one.

Matching a specific request

When several requests are outstanding and responses may come back in a different order, pass the transaction ID of the request you are waiting for:


get_response(m_rsp, req.get_transaction_id());      // waits for this request's response only

Use this only for a response that is guaranteed to arrive. If the driver has already returned that response and it was collected by an earlier call, get_response() waits forever.

Quick Check

Q: The driver creates m_rsp with bus_rsp::type_id::create(), fills in the data and calls item_done(m_rsp), but leaves out m_rsp.set_id_info(m_req). What happens?

Show Answer

The sequencer cannot tell which sequence the response belongs to, because a new object has no sequence ID. It stops the simulation with a fatal error instead of delivering the response. The fix is one line: m_rsp.set_id_info(m_req) before returning it.

Handling Responses Asynchronously

Waiting for each response before sending the next request serializes everything. A sequence that wants to send requests back to back and process results as they arrive can switch to the response handler instead. After use_response_handler(1), the sequencer calls the sequence's response_handler() function for every response, and nothing goes into the response queue.


class stream_seq extends uvm_sequence #(bus_item);
  `uvm_object_utils(stream_seq)
  int unsigned n_rsp, n_err;
  // Rest of the code (constructor)

  virtual function void response_handler(uvm_sequence_item response);
    bus_item r;
    if (!$cast(r, response)) `uvm_fatal("SEQ", "Unexpected response type")
    n_rsp++;
    if (!r.ok) n_err++;
  endfunction

  virtual task body();
    use_response_handler(1);                        // before the first request
    repeat (50) begin
      req = bus_item::type_id::create("req");
      start_item(req);
      if (!req.randomize()) `uvm_error("SEQ", "Randomization failed")
      finish_item(req);                             // no get_response() here
    end
    wait (n_rsp == 50);                             // all responses handled
    `uvm_info("SEQ", $sformatf("%0d responses, %0d errors", n_rsp, n_err), UVM_LOW)
  endtask
endclass

response_handler() is a function, so it must not wait or consume time. Keep it to bookkeeping such as counting, storing or flagging, and do anything time-consuming in body(). Enable the handler at the top of body() rather than in pre_body(), because pre_body() is skipped when a parent starts this sequence with a `uvm_do macro.

The Response Queue and Overflow

Each sequence's response queue holds 8 responses by default. If the driver returns responses faster than the sequence calls get_response(), or the sequence never calls it at all, the ninth response overflows the queue. UVM drops it and reports an error.

That is the most common response problem: a driver that always returns a response, combined with a sequence written for the no-response style. There are three clean fixes:

  • Collect every response with get_response(), as read_seq does.
  • Use response_handler(), as stream_seq does.
  • If the sequence genuinely doesn't care about responses, call set_response_queue_depth(-1) for an unlimited queue, or set_response_queue_error_report_disabled(1) to drop overflow silently. Use these deliberately, because they also hide real bugs.

Different Request and Response Types

The response doesn't have to be the same class as the request. A read request may only need an address, while the response carries data and a status. Give the second parameter to the sequence, the sequencer and the driver:


class rd_seq extends uvm_sequence #(bus_req, bus_rsp);   // get_response() returns bus_rsp
// ...
typedef uvm_sequencer #(bus_req, bus_rsp) bus_sequencer;
// ...
class bus_driver extends uvm_driver #(bus_req, bus_rsp);
  // inside run_phase:
  //   m_rsp = bus_rsp::type_id::create("m_rsp");
  //   m_rsp.set_id_info(m_req);
  //   seq_item_port.item_done(m_rsp);

All three must agree on both types, otherwise the connection between driver and sequencer does not compile.

Which Driver Call to Use

  • item_done(rsp) finishes the current request and returns the response in one call. It suits drivers that complete one request at a time.
  • put_response(rsp) returns a response without finishing a request. A driver that calls item_done() early and returns results later uses this. With the get() style of handshake, put(rsp) plays the same role, as described in Using get() and put().
  • rsp_port.write(rsp) sends the response through the driver's analysis port. It needs m_driver.rsp_port.connect(m_sequencer.rsp_export) in the agent, and is mostly found in drivers that already separate request and response handling into different threads.

Common Mistakes

Mistake #1: Forgetting set_id_info()

Without the request's IDs, the sequencer cannot route the response and the test ends in a fatal error. Call rsp.set_id_info(req) on every response, even when the response is a clone of the request, so the code keeps working if the response type changes later.

Mistake #2: Returning responses nobody collects

A driver that always calls item_done(rsp) overflows the queue of any sequence that doesn't call get_response(), after eight items. Make the driver and its sequences agree on whether responses are used.

Mistake #3: Calling get_response() with the handler enabled

Once use_response_handler(1) is set, responses go only to response_handler(). A get_response() call in the same sequence waits forever.

Mistake #4: Doing time-consuming work in response_handler()

response_handler() is a function called by the sequencer, so it cannot wait on events or clock edges. Record the response there and act on it in body().

Summary

In this article, you learned:

  • A driver returns a response with item_done(rsp), put_response(rsp) or rsp_port, always after rsp.set_id_info(req)
  • A sequence collects responses in order with get_response(), or by transaction ID
  • use_response_handler(1) sends responses to response_handler() instead of the queue
  • The response queue holds 8 responses by default, and overflow is reported as an error
  • Request and response can be different classes if the sequence, sequencer and driver all agree
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5