Every DMA test so far has passed, and every one of them ran with the output port's TREADY tied high. The DMA was never once told to wait. Yet the design has a whole piece of logic for exactly that case: an output register, an out_free term, and an input s_tready that drops when the output is stuck. None of it has ever been exercised. A designer asks you to stall the output at random and prove no word is lost or duplicated. The DUT starts every beat; the bench must answer. How do you build an agent that responds instead of commands?
A master agent starts transactions: its sequence decides what happens next and its driver makes it happen. A slave (or reactive) agent is the other way round. The DUT starts each transaction, and the agent decides how to answer: how long to hold TREADY low, what read data to return, whether to send an error response. Think of a hotel reception desk. Guests (the DUT) walk up whenever they like; the receptionist (the driver) handles the exchange at the counter; the manager's policy book (the responder sequence) says whether to serve at once or ask them to wait.
The reason to keep the policy in a sequence, and not hard-code it in the driver, is the same reason stimulus lives in sequences: a test can change the policy without touching the agent. No stalls, short random stalls, long bursts of backpressure, or a stall pattern that targets one corner of the DUT are all just different sequences (or a factory override of the item's constraints) running on the same driver.
There are two common ways to tell the responder that a request has arrived. In the shape the bench uses, the driver waits for the request: it watches the pins until the DUT offers a beat, then calls get_next_item to ask the sequence what to do. The responder never needs to know anything about the request, because a TREADY stall does not depend on the data being offered.
In the other shape, a monitor detects the request and writes it into an analysis FIFO on the sequencer; the responder blocks on that FIFO, reads the request, and builds a response that depends on it. That is the shape you need when the answer depends on the request, as with read data for an address. It is taught in full, sequence side first, in Reactive sequences.
Driver waits for the request | Monitor feeds a FIFO on the sequencer | |
|---|---|---|
Response depends on the request? | No (stall length, random ready) | Yes (read data, per-address errors) |
Extra parts | None | Request monitor, FIFO, p_sequencer handle |
Driver knowledge | Must recognise a request on the pins | Drives only what the item says |
Coverage of requests | Needs a separate monitor | The request monitor feeds coverage too |
The item is the answer, not the question: how many clocks to keep TREADY low before taking the beat. The distribution favours no stall but includes runs of up to six clocks. The responder is an ordinary start_item / randomize / finish_item loop with no end.
// snippet: from the course DMA bench
// A sink item says how long the slave makes the master wait before taking a beat.
class axis_sink_item extends uvm_sequence_item;
rand int unsigned stall;
constraint stall_c { stall dist { 0 := 5, [1:2] := 3, [3:6] := 2 }; }
`uvm_object_utils(axis_sink_item)
function new(string name = "axis_sink_item");
super.new(name);
endfunction
endclass
// The responder: answers every beat, forever. Start it in a fork; it never returns.
class axis_ready_seq extends uvm_sequence #(axis_sink_item);
`uvm_object_utils(axis_ready_seq)
function new(string name = "axis_ready_seq");
super.new(name);
endfunction
task body();
forever begin
axis_sink_item it = axis_sink_item::type_id::create("it");
start_item(it);
if (!it.randomize()) `uvm_fatal("RAND", "axis_sink_item randomize failed")
finish_item(it);
end
endtask
endclassThe driver drives with nonblocking assignments after @(posedge clk), the same rule as every driver on the bench. It loops on the clock until TVALID is high, which reads the value from before the edge: the value the DUT drove on the previous cycle. Only then does it call get_next_item, so the item it gets answers this beat and no other. It counts stall clocks so the test can report how hard it pushed.
// snippet: from the course DMA bench
// A reactive driver: it waits for the master to offer a beat (TVALID), and only
// then asks its sequence how to answer: hold TREADY low for `stall` clocks, then
// accept the beat.
class axis_sink_driver extends uvm_driver #(axis_sink_item);
`uvm_component_utils(axis_sink_driver)
virtual axis_if vif;
int unsigned stall_clocks;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual axis_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "axis_if not set for this sink driver")
endfunction
task run_phase(uvm_phase phase);
vif.tready <= 1'b0;
forever begin
do @(posedge vif.clk); while (!vif.tvalid); // a beat is on offer
seq_item_port.get_next_item(req); // ask the sequence how to respond
repeat (req.stall) @(posedge vif.clk);
stall_clocks += req.stall;
vif.tready <= 1'b1;
@(posedge vif.clk); // the beat moves on this edge
vif.tready <= 1'b0;
seq_item_port.item_done();
end
endtask
endclassaxis_sink_agent extends the bench's axis_agent. It inherits the monitor and the analysis port, so the scoreboard keeps receiving output words exactly as before, and adds a second sequencer and driver for the slave side. The env had already made out_agt passive, so the master-side sequencer and driver are still not built; the slave side is built unconditionally. A production VIP would gate it on its own configuration knob (for example cfg.slave_mode), because is_active alone cannot describe an agent that is passive as a master but active as a responder.
// snippet: from the course DMA bench
// The output agent with a slave side: the same monitor as axis_agent, plus a
// sink sequencer and driver that answer the DUT's beats. It always builds the
// slave side; the monitor is all a passive agent would have had.
class axis_sink_agent extends axis_agent;
`uvm_component_utils(axis_sink_agent)
axis_sink_sequencer sink_sqr;
axis_sink_driver sink_drv;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
sink_sqr = axis_sink_sequencer::type_id::create("sink_sqr", this);
sink_drv = axis_sink_driver::type_id::create("sink_drv", this);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
sink_drv.seq_item_port.connect(sink_sqr.seq_item_export);
endfunction
endclassThe env builds in_agt and out_agt from the same axis_agent class. A type override would turn both into sink agents, and the input side would start answering its own driver. An instance override with the test as context replaces only uvm_test_top.env.out_agt. It is set before super.build_phase creates the env, the test casts env.out_agt to reach the new sequencer, and the responder is started in fork ... join_none because it never returns.
// snippet: from the course DMA bench
// The output port gets a reactive slave with random backpressure: an instance
// override swaps the agent at env.out_agt only; env.in_agt keeps axis_agent.
class dma_backpressure_test extends dma_base_test;
`uvm_component_utils(dma_backpressure_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
axis_agent::type_id::set_inst_override(axis_sink_agent::get_type(), "env.out_agt", this);
super.build_phase(phase);
endfunction
task run_phase(uvm_phase phase);
axis_sink_agent sink;
axis_ready_seq rsp = axis_ready_seq::type_id::create("rsp");
dma_job_cfg job = dma_job_cfg::type_id::create("job");
bit [31:0] status;
phase.raise_objection(this);
if (!$cast(sink, env.out_agt)) `uvm_fatal("CAST", "env.out_agt is not an axis_sink_agent")
fork
rsp.start(sink.sink_sqr); // the responder runs for the whole test
join_none
if (!job.randomize() with { len == 12; }) `uvm_fatal("RAND", "dma_job_cfg randomize failed")
run_job(job, 12, status);
if (status[2:1] !== 2'b01) `uvm_error("STATUS", $sformatf("expected DONE only, STATUS=%08h", status))
#200ns;
`uvm_info("BP", $sformatf("the slave stalled the DMA for %0d clocks", sink.sink_drv.stall_clocks), UVM_LOW)
phase.drop_objection(this);
endtask
endclassBefore this agent existed, something had to drive the DUT's m_tready. The block top does it with a procedural default: initial m_ax.tready = 1'b1;. That one line is a deliberate choice. A continuous assign m_ax.tready = 1'b1 would make the signal permanently owned by the top, and SystemVerilog forbids mixing a continuous assignment with procedural writes to the same variable. With an initial block, the sink driver's first vif.tready <= 1'b0 simply takes over: procedural writes from different processes are legal, and the last one wins. Tests that do not add the sink agent keep the default of 1.
It works, but it is a convention, and conventions need writing down. Two other ownership models scale better in a VIP: the agent drives a configurable idle value itself whenever no responder is running, or the interface has a mux (assign tready = slv_en ? slv_tready : 1'b1;) that a configuration bit selects. Either way, the rule is one owner per signal per test, and the top or the agent says who it is.
When the DUT reads and writes memory over a bus, the responder becomes a memory model: an associative array keyed by address. A write request updates it; a read request is answered from it, with a random or poisoned value for an address never written. Because the answer depends on the request, this is the monitor-and-FIFO shape. Keep the memory in the responder sequence or in a small object the sequence holds, so a test can preload it (a backdoor mem[addr] = data before the run) and the scoreboard can read it to predict what the DUT should have fetched.
// snippet: the core of a memory responder (monitor-and-FIFO shape)
forever begin
p_sequencer.req_fifo_mon.get(req); // a read or write the monitor saw requested
rsp = mem_rsp_item::type_id::create("rsp");
start_item(rsp);
if (req.write) mem[req.addr] = req.wdata;
else rsp.rdata = mem.exists(req.addr) ? mem[req.addr] : $urandom;
if (!rsp.randomize() with { wait_states inside {[0:max_wait]}; })
`uvm_fatal("RAND", "mem_rsp_item randomize failed")
finish_item(rsp); // returns when the driver has answered
endWait states, error responses (SLVERR on a protected range) and out-of-order completion all become constraints or fields of the response item, so each test picks its own mix without a new driver.
Forever: the DUT can offer a beat at any time, so something must always be ready to answer. A responder that stops after N items leaves the DUT stalled for good the first time a job is longer than N.
Forked: rsp.start(...) never returns. Called inline, nothing after it runs. Many VIPs start a default responder from the agent's own run_phase when a config bit is set, so no test can forget it.
No objection: a responder has no natural end. End of test belongs to the test and the scoreboard; a responder that raised an objection would hold every test open until the timeout.
Reset: a reset kills or restarts the responder like any other sequence; the next lesson shows the driver side of that.
dma_backpressure_test runs one 12-word job. With +UVM_VERBOSITY=UVM_MEDIUM:
UVM_INFO dma_tb.sv(1246) @ 1005000: uvm_test_top [BP] the slave stalled the DMA for 28 clocks
UVM_INFO dma_tb.sv(411) @ 1005000: uvm_test_top.env.sb [SB] 12 words matched, 0 mismatched
UVM_ERROR : 0
UVM_FATAL : 028 stall clocks over 12 beats means the output register really filled and s_tready really dropped. The stall count is random, so it differs by simulator and seed.
12 words matched with an untouched scoreboard: the monitor that axis_sink_agent inherited still reports every accepted beat, stalls or not.
The topology printed at the start shows out_agt as axis_sink_agent with sink_sqr and sink_drv, while in_agt is still a plain axis_agent. That line is your proof the instance override hit one agent only.
A team added a slave agent like this one to a stream block and celebrated: random stalls, thousands of beats, no mismatches. Months later a bug escaped in which the block dropped a word when it received back-to-back beats with the output ready the whole time. Every backpressure test passed over it, and the tie-off tests never stalled. Coverage of consecutive accepted beats on the output port was zero in every test that used the slave agent.
The cause is visible in axis_sink_driver too. It raises TREADY only after it has seen TVALID, then drops it after one beat and waits for TVALID again. Even with stall == 0, every beat costs at least two clocks, and ready-before-valid, which AXI-Stream explicitly allows, never happens. A reactive slave adds latency by construction. The fix is a response item field such as ready_before_valid, which tells the driver to hold TREADY high while idle and take beats on consecutive clocks, plus a coverpoint on the gap between accepted beats so the hole cannot hide again.
axis_agent::type_id::set_type_override(axis_sink_agent::get_type()) instead of the instance override. What happens?Both in_agt and out_agt become sink agents. On the input side the extra sink driver starts writing TREADY, a signal the DUT itself drives there (s_tready), so the bench and the DUT both claim the same signal and the input stalls are no longer the DUT's own. An instance override at env.out_agt with the test as context changes only the output side.
stall == 0. Does the output now run at full throughput?No. The driver still waits to see TVALID before raising TREADY and drops TREADY after every beat, so each beat takes at least two clocks. Full throughput needs the driver to hold TREADY high before TVALID arrives, which is a different response behaviour, not a smaller stall.
Next: the slave agent now pushes back on the DUT. The other thing real systems do is pull the floor out from under a transfer: Reset on the fly makes every component survive a reset in the middle of a job.