A UVM reactive sequence, also called a responder or slave sequence, answers transfers that the DUT starts instead of starting them itself. It runs in a forever loop: it sends an empty request item to the driver, the driver fills it in when the DUT makes a request on the bus, the sequence decides how to respond, and it sends a second item that the driver uses to complete the transfer.

Responders are needed whenever the DUT is the bus master: a DMA engine reading memory, a processor fetching instructions, or a bridge forwarding traffic. The testbench has to play the slave, and putting the decisions about how to answer in a sequence makes them easy to change per test, for example to add wait states or inject an error response.

8 min read | Intermediate

What You'll Learn

  • Describe the four-step handshake between a responder sequence and a slave driver
  • Write a slave driver that waits for the DUT and reports what it saw
  • Write a responder sequence with a memory model and randomized responses
  • Start a never-ending sequence without blocking the end of the test

How a Responder Works

In an initiator agent, the sequence knows what it wants to do and the driver makes it happen. In a responder agent, nobody in the testbench knows when the next request will come or what it will be. The trick is to invert the flow of information through the same get_next_item() and item_done() handshake:

  • Step 1: the sequence sends an empty request item, which effectively means "tell me what to do".
  • Step 2: the driver holds that item until the DUT starts a transfer, copies the request from the pins into it, and calls item_done(). The sequence's finish_item() returns with the request filled in.
  • Step 3: the sequence decides on the response, updates its memory model if needed, and sends a second item carrying the response.
  • Step 4: the driver drives the response onto the bus, completes the transfer, calls item_done(), and the loop starts again.

The Sequence Item

In a responder item the roles of the fields are the reverse of an initiator item. Fields that come from the DUT are not random, and fields the testbench chooses are rand with constraints:


class slv_item extends uvm_sequence_item;
  // From the DUT (filled in by the driver, never randomized)
  bit [7:0]  addr;
  bit        write;
  bit [31:0] wdata;
  // Chosen by the testbench
  rand bit [31:0] rdata;
  rand int        delay;                  // wait states before ready

  constraint c_delay { delay inside {[0:3]}; }

  `uvm_object_utils_begin(slv_item)
    `uvm_field_int(addr,  UVM_ALL_ON)
    `uvm_field_int(write, UVM_ALL_ON)
    `uvm_field_int(wdata, UVM_ALL_ON)
    `uvm_field_int(rdata, UVM_ALL_ON)
    `uvm_field_int(delay, UVM_ALL_ON | UVM_DEC)
  `uvm_object_utils_end
  // Rest of the code (constructor)
endclass

The Slave Driver

The bus in this example is simple: the DUT raises sel with an address, a direction and write data, and holds them until the slave raises ready for one clock. For reads, rdata must be valid while ready is high.


class slv_driver extends uvm_driver #(slv_item);
  `uvm_component_utils(slv_driver)
  virtual slv_if vif;
  // Rest of the code (constructor, build_phase getting vif)

  virtual task run_phase(uvm_phase phase);
    slv_item m_req, m_rsp;
    vif.ready <= 0;
    forever begin
      // Steps 1 and 2: wait for the DUT, report what it asked for
      seq_item_port.get_next_item(m_req);
      @(posedge vif.clk iff vif.sel);
      m_req.addr  = vif.addr;
      m_req.write = vif.write;
      m_req.wdata = vif.wdata;
      seq_item_port.item_done();

      // Steps 3 and 4: drive the response the sequence chose
      seq_item_port.get_next_item(m_rsp);
      repeat (m_rsp.delay) @(posedge vif.clk);
      if (!m_rsp.write) vif.rdata <= m_rsp.rdata;
      vif.ready <= 1;
      @(posedge vif.clk);
      vif.ready <= 0;
      seq_item_port.item_done();
    end
  endtask
endclass

The driver contains no decisions at all. It doesn't know what data to return or how long to wait; it only translates between pins and items. Everything that varies from test to test lives in the sequence.

The Responder Sequence

This responder behaves like a simple memory. Writes are stored in an associative array, and reads return the stored value, or zero for an address that was never written.


class mem_responder_seq extends uvm_sequence #(slv_item);
  `uvm_object_utils(mem_responder_seq)
  bit [31:0] mem [bit [7:0]];               // the memory model
  // Rest of the code (constructor)

  virtual task body();
    slv_item   m_req, m_rsp;
    bit [31:0] rd;
    forever begin
      // Step 1: ask the driver for the next request
      m_req = slv_item::type_id::create("m_req");
      start_item(m_req);
      finish_item(m_req);                    // step 2: returns when the DUT made a request

      if (m_req.write) mem[m_req.addr] = m_req.wdata;
      rd = mem.exists(m_req.addr) ? mem[m_req.addr] : 32'h0;

      // Step 3: build and send the response
      m_rsp = slv_item::type_id::create("m_rsp");
      start_item(m_rsp);
      m_rsp.copy(m_req);                     // carry addr, write, wdata across
      if (!m_rsp.randomize() with { rdata == rd; })
        `uvm_error("RESP", "Randomization failed")
      finish_item(m_rsp);                    // step 4: returns when the transfer is done
    end
  endtask
endclass

The read value is computed into rd before randomization and fixed with an inline constraint, while delay stays random. Copying the request into the response with copy(), which the field macros provide, gives the driver the direction it needs to decide whether to drive rdata.

Changing the behaviour per test

Because all the decisions are in the sequence, a test that wants different behaviour simply runs a different sequence, or overrides this one through the factory. A sequence that always adds the maximum wait state, or that corrupts the data at one address, is a few lines of code and needs no change to the agent.

Quick Check

Q: Why does the sequence send an empty item in step 1, rather than the driver just waiting for the DUT on its own and asking the sequence afterwards?

Show Answer

A driver can only receive information from a sequence through items, not ask it questions. Sending an empty item first gives the driver something to fill in and return through item_done(), which is the only way to pass the DUT's request back to the sequence and unblock its finish_item().

Starting a Responder

A responder never finishes, so it must not be started the way a normal sequence is started between a raised and a dropped objection. That would keep the test running forever. Start it in a separate thread instead, and let the initiator traffic decide when the test ends:


virtual task run_phase(uvm_phase phase);
  mem_responder_seq resp = mem_responder_seq::type_id::create("resp");
  dma_test_seq      main = dma_test_seq::type_id::create("main");

  fork
    resp.start(m_env.m_slv_agent.m_seqr);   // runs for the whole test, no objection
  join_none

  phase.raise_objection(this);
  main.start(m_env.m_cfg_agent.m_seqr);     // programs the DUT and waits for it to finish
  phase.drop_objection(this);
endtask

When the last objection is dropped and the run phase ends, UVM terminates the processes started from it, including the responder's loop. Because the responder is usually waiting for the next request at that point, this ending is clean.

Another common option is to make the responder the default sequence of the slave sequencer, so every test gets it automatically and only tests with special needs replace it.

A responder agent often keeps a pointer to a shared memory model instead of its own associative array, so that the scoreboard and the responder agree on the memory contents. The handshake stays exactly the same.

Common Mistakes

Mistake #1: Raising an objection in the responder

A forever loop that raises an objection never drops it, so the test never ends. Responders run without objections; the initiator side controls the end of test.

Mistake #2: Randomizing the request fields

The address, direction and write data come from the DUT. If they are declared rand, randomizing the response overwrites what the DUT asked for. Only the testbench's choices should be random.

Mistake #3: Putting decisions in the driver

A driver that chooses read data or delays itself can only be changed by editing the agent. Keep the driver to pin translation, and keep every decision in the sequence.

Mistake #4: Reusing one item for request and response

The request item is handed back to the sequence by item_done(). Building the response in a separate object keeps the request intact for logging and for checks, and avoids surprises if the driver still holds a handle.

Summary

In this article, you learned:

  • A responder sequence answers transfers started by the DUT, using two items per transfer
  • The empty request item lets the driver report the DUT's request back through item_done()
  • Request fields are plain variables; response fields are rand with constraints
  • The slave driver only translates between pins and items, and all behaviour lives in the sequence
  • Responders run in a separate thread without an objection, and stop when the run phase ends
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5