The base bench's output stream accepts every beat (initial m_ax.tready = 1'b1 in the top): the DMA can always send. Real downstream logic stalls, and the DMA's output register and its s_tready logic exist precisely to cope with that. No test has ever exercised it. To make TREADY random, the bench must react to the DUT: wait until it offers a beat, then decide how long to make it wait. Who decides, a driver or a sequence, and how does the sequence even know a beat is being offered?
So far, every sequence has been the active side: it decides what happens and the DUT responds. On a slave (or reactive) interface the roles flip. The DUT starts each transaction, and the bench answers: with TREADY after some delay, with read data, with an error response. A responder sequence is the sequence that answers. It runs forever, waits for a request, decides on a response, and sends it to the driver as an item.
Think of a shop counter. The customer (the DUT) walks up; the shop assistant (the driver) handles the physical exchange; the manager in the back (the responder sequence) decides the policy: serve at once, or make them wait two minutes. The manager needs someone to say a customer has arrived, and that is the monitor's job. Keeping the decision in a sequence, not the driver, means a test can change the policy (no stalls, random stalls, long stalls, errors) by starting a different sequence or overriding one, with the same agent.
The item is the response: how many clocks to hold TREADY low. The sequencer gets an analysis FIFO, so a monitor can push requests into it and a sequence running on it can reach them through p_sequencer.
// snippet
class axis_ready_item extends uvm_sequence_item;
rand int unsigned delay; // clocks to hold TREADY low
bit [31:0] data; // the beat that was offered
constraint delay_c { delay dist { 0 := 4, [1:3] := 4, [4:6] := 2 }; }
`uvm_object_utils(axis_ready_item)
function new(string name = "axis_ready_item");
super.new(name);
endfunction
endclass
class axis_slave_sequencer extends uvm_sequencer #(axis_ready_item);
`uvm_component_utils(axis_slave_sequencer)
uvm_tlm_analysis_fifo #(axis_item) offer_fifo;
function new(string name, uvm_component parent);
super.new(name, parent);
offer_fifo = new("offer_fifo", this);
endfunction
endclassThis monitor reports offers, not completed transfers: the first clock on which TVALID is high for a beat that has not yet been taken. It samples through the mon_cb clocking block like every monitor on the bench, and creates a fresh object for each offer.
// snippet: run_phase of axis_req_monitor
task run_phase(uvm_phase phase);
bit pending;
forever begin
@(vif.mon_cb);
if (vif.mon_cb.tvalid && vif.mon_cb.tready) pending = 0; // beat taken
else if (vif.mon_cb.tvalid && !pending) begin // a new offer
axis_item r = axis_item::type_id::create("r");
r.data = vif.mon_cb.tdata;
r.last = vif.mon_cb.tlast;
pending = 1;
req_ap.write(r);
end
end
endtaskThe responder blocks on the FIFO, then does an ordinary start_item / randomize / finish_item. The finish_item returns when the driver calls item_done, which the driver does after the handshake, so the loop answers exactly one offer at a time.
// snippet
class axis_ready_responder_seq extends uvm_sequence #(axis_ready_item);
`uvm_object_utils(axis_ready_responder_seq)
`uvm_declare_p_sequencer(axis_slave_sequencer)
int unsigned n_served, stall_clocks;
function new(string name = "axis_ready_responder_seq");
super.new(name);
endfunction
task body();
forever begin
axis_item offered;
axis_ready_item rsp;
p_sequencer.offer_fifo.get(offered); // block until the DUT offers a beat
rsp = axis_ready_item::type_id::create("rsp");
start_item(rsp);
if (!rsp.randomize()) `uvm_fatal("RAND", "axis_ready_item randomize failed")
rsp.data = offered.data;
finish_item(rsp); // returns after the handshake
n_served++;
stall_clocks += rsp.delay;
end
endtask
endclassThe driver is the bench's usual shape: wait on @(posedge clk), drive with nonblocking assignments. It holds TREADY low for delay clocks, raises it, and waits for the clock edge where TVALID was also high. In the agent's connect_phase, the monitor's port goes to the FIFO and the driver to the sequencer.
// snippet
// axis_ready_driver::run_phase
vif.tready <= 1'b0;
forever begin
seq_item_port.get_next_item(req);
repeat (req.delay) @(posedge vif.clk);
vif.tready <= 1'b1;
do @(posedge vif.clk); while (!vif.tvalid); // the beat moves on TVALID && TREADY
vif.tready <= 1'b0;
seq_item_port.item_done();
end
// axis_slave_agent::connect_phase
mon.req_ap.connect(sqr.offer_fifo.analysis_export);
drv.seq_item_port.connect(sqr.seq_item_export);
// the test's run_phase: the responder lives as long as the test
fork
resp.start(slv.sqr);
join_noneFor this run, the slave's driver takes over TREADY from the top's initial m_ax.tready = 1'b1 default, and its vif is the output interface. The bench now ships this pattern as axis_sink_agent (see Reactive slave agents). The bench's passive output agent and scoreboard are untouched: they keep checking every word, now with stalls.
Without a monitor, the driver itself can watch for the request and then ask the sequence what to do. The responder sequence calls start_item early, the driver's get_next_item returns an empty item, the driver waits on the pins for the request, fills the request fields into the item and calls item_done; the sequence reads the request after finish_item and sends a second item with the response. It needs no FIFO, but every item is a two-step exchange and the driver must understand requests. The monitor-and-FIFO shape keeps the driver dumb and lets the same monitor feed coverage.
A memory-model slave is the same loop with state. For a bus where the DUT reads and writes memory, the responder keeps bit [31:0] mem[bit [31:0]]: a write request updates it, a read request is answered with mem[addr] (or a random value for an address never written). The sequence is the memory; the driver only moves bits.
A scratch run of the bench with the slave agent added (dma_backpressure_test, one 8-word job with key 0x12345678). The stall clocks are random:
UVM_INFO dma_tb_scratch.sv(1307) @ 765000: uvm_test_top [RESP] answered 8 beats, 15 stall clocks, STATUS=00000002
UVM_INFO dma_tb.sv(411) @ 765000: uvm_test_top.env.sb [SB] 8 words matched, 0 mismatched
UVM_ERROR : 08 beats answered for 8 words: one response per offer, no duplicates from the monitor and none missed.
15 stall clocks of backpressure, and still 8 words matched: the DMA held each output word stable while TREADY was low and lost nothing.
STATUS=00000002: DONE alone. Backpressure on the output did not cause an error.
A responder runs forever, so resp.start(slv.sqr) never returns. Call it directly in the test's run_phase, and nothing after it runs: no job is programmed, the objection never drops, and the test ends only at the timeout. A scratch test that does exactly that, with a 20 us timeout:
UVM_FATAL uvm_phase_hopper.svh(598) @ 20000000: reporter [PH_TIMEOUT] Explicit timeout of 20000000 hit, indicating a probable testbench issueStart responders in fork ... join_none (or from the agent or env, so tests never forget). Do not raise an objection for them: a responder has no end, and end of test belongs to the test and the scoreboard.
The first version of the slave sequencer named its FIFO req_fifo. The run died at time 0:
UVM_FATAL uvm_component.svh(1761) @ 0: req_fifo [CLDEXT] Cannot set 'req_fifo' as a child of 'uvm_test_top.slv.sqr', which already has a child by that name.uvm_sequencer already has a child component called req_fifo (its internal request FIFO), and component names must be unique among siblings. Renaming the member's instance name to offer_fifo fixed it. It is a good reminder that a class you extend comes with children you did not write.
Every stalled clock produces another request, so the responder answers the same beat several times. The driver then raises TREADY for beats that do not exist yet, and the delays no longer mean what the sequence chose. The pending flag makes one request per beat.
Kill the first responder after the first job and start a second one with a different delay constraint (for example a derived item type through a factory override, or a sequence with a randomize() with on the delay). The monitor, FIFO and driver are unchanged.
Next: that completes coordination across agents. Return to the topic map to review, or continue to TLM and connectivity, where the analysis FIFO used here is explained in full.