Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations14
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
    • Agent Architecture
    • Reusable VIP: Active, Passive and Protocol Agents
    • Error and Fault Injection
    • Agent Patterns: Slaves, Reset, Interrupts and Pipelines
      • Reactive Slave Agents
      • Reset on the Fly
      • Interrupt Agents
      • Pipelined and Out-of-Order Drivers
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹Agent Patterns: Slaves, Reset, Interrupts and Pipelines
  1. Topics
  2. UVM Verification
  3. UVM Agents & Protocol VIP
  4. Agent Patterns: Slaves, Reset, Interrupts and Pipelines
  5. Reactive Slave Agents

Part 6 · Agents & Protocol IP · Intermediate

Reactive Slave Agents

A slave agent that answers the DUT: the driver waits for a request, asks a forever-running responder sequence how to answer, and drives the response.

On this page

    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?

    The idea: the bench answers, the policy lives in a sequence

    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.

    The picture: one beat on the output stream

    Sequence diagram: the responder sequence offers an item, the DUT raises TVALID, the driver takes the item, stalls for the chosen number of clocks, raises TREADY for one clock, then calls item_done so the responder loops.
    The driver-waits-for-request shape used by the bench. The responder's item is ready before the beat arrives; the driver only takes it once the DUT offers a beat.

    Two shapes of reactive agent, and when each fits

    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

    Build it: the slave side of the output agent

    The item and the responder

    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.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // 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
    endclass

    The driver: wait for TVALID, then ask

    The 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.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    // 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
    endclass

    The agent: extend, do not fork

    axis_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.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 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
    endclass

    The test: an instance override that hits one agent

    The 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.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    // 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
    endclass

    Note

    The context argument matters. With this, the path "env.out_agt" is relative to the test. Without it, the string is taken as a full path and matches nothing, so the plain agent is built and the test silently runs with no backpressure. The rules for override order are in Overriding items and sequences.

    Who owns TREADY now?

    Before 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.

    Memory-model slaves: the same loop, with state

    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.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 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
    end

    Wait 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.

    Why the responder runs forked, forever, with no objection

    • 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.

    Watch it run

    dma_backpressure_test runs one 12-word job. With +UVM_VERBOSITY=UVM_MEDIUM:

    Diagram
    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 :    0
    • 28 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.

    What breaks: the backpressure agent that hid a coverage hole

    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.

    Interview

    "Your agent must act as an AXI slave with a memory and random wait states. Walk me through the components and who owns what."

    A weak answer: "The driver waits for a request, looks up the memory and drives the response." Every new policy then means editing the driver, and the memory is invisible to the scoreboard and the test.

    A strong answer: "A monitor detects each request on the address channel and writes it to a FIFO on the slave sequencer. A responder sequence runs forked for the whole test with no objection; it gets each request, reads or updates a memory object, randomizes wait states and the response code, and sends a response item. The driver only drives what the item says. The test preloads the memory through a handle and the scoreboard reads the same memory to predict. I swap the slave in with an instance override so only that port changes, and I cover ready-before-valid and back-to-back cases because a reactive driver adds latency."

    Follow-ups to expect: Why not put the policy in the driver? (tests change policy by sequence or override, the driver stays one class) and How does the responder stop at reset? (killed or restarted with the rest of the stimulus, while the driver parks the bus).

    Check yourself A test uses 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.

    Check yourself The responder's item constraint is changed to 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.

    Related topics

    • Reactive sequences (the sequence side)

    • The driver handshake

    • Overriding items and sequences

    • Reset on the fly

    Previous

    111 / 280

    Next: Reset on the Fly

    Up next

    Up nextReset on the Fly

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Interrupt Agents
    • Pipelined and Out-of-Order Drivers