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. Pipelined and Out-of-Order Drivers

Part 6 · Agents & Protocol IP · Intermediate

Pipelined and Out-of-Order Drivers

The component side of pipelining: separate address and data threads, get versus get_next_item, put_response with set_id_info, and an outstanding table keyed by ID.

On this page

    The memory controller behind an AXI port takes about 100 cycles to return read data, and it is designed to keep 16 reads in flight and return them in whatever order its banks finish. Your agent's driver does get_next_item, drives the address, waits for the data, and calls item_done. It is correct, and it issues one read every 100 cycles. The reorder logic, the ID tracking and the read-data interleaving in the DUT are never touched. The interviewer leans in: "How would you write a driver that keeps several reads outstanding and copes with data coming back out of order?"

    The idea: a kitchen ticket rail

    A busy restaurant kitchen does not cook one order at a time. The waiter clips each ticket to a rail as the order comes in and walks away to take the next one. Dishes leave the pass in whatever order they finish, and each one is matched to its ticket by table number before it goes out. A pipelined driver works the same way. An address thread takes requests and issues them on the request channel, clipping each to an outstanding table. A separate data thread watches the response channel, finds the matching entry by ID, and sends the answer back to the sequence that asked.

    The sequence side of this (sending many requests before collecting responses, or using a response handler) is taught in Responses and pipelined drivers. This lesson builds the driver: the threads, the table, the handshake calls, and what goes wrong in each.

    The picture: four reads, answered out of order

    Sequence diagram: the sequence hands four reads to the address thread, which issues them all; the slave returns data for id 3, then id 2, then both id 1 reads in order; the data thread sends a response for each back to the sequence.
    The scratch run: the slave answers later addresses sooner, so responses come back 08, 04, 00, 0c. The two id=1 reads still return in order.

    Build it

    The example is an AXI-style read channel: an AR (address) channel and an R (data) channel, each with its own valid/ready handshake, and a 4-bit ID on both. Single-beat reads keep it short; bursts add a beat counter per entry but no new ideas. It ran as a scratch test alongside the DMA bench, with a small read slave that gives later addresses shorter latency, so data returns out of order.

    The item and the outstanding table

    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
    // snippet: a pipelined read driver (scratch run alongside the DMA bench)
    class axi_rd_item extends uvm_sequence_item;
      rand bit [3:0]  id;
      rand bit [31:0] addr;
      bit [31:0]      rdata;                     // filled in by the response
      `uvm_object_utils(axi_rd_item)
      function new(string name = "axi_rd_item");
        super.new(name);
      endfunction
    endclass
    
    class axi_rd_driver extends uvm_driver #(axi_rd_item);
      `uvm_component_utils(axi_rd_driver)
      virtual axi_rd_if vif;                     // arid, araddr, arvalid, arready, rid, rdata, rvalid, rready
      int unsigned max_outstanding = 4;
      axi_rd_item  pending[int][$];              // outstanding reads: a queue per ID
      int unsigned n_pending;
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
    
      task run_phase(uvm_phase phase);
        vif.arvalid <= 1'b0;
        vif.rready  <= 1'b0;
        @(posedge vif.rst_n);
        vif.rready  <= 1'b1;
        fork
          addr_thread();
          data_thread();
        join
      endtask

    The table is an associative array of queues: pending[id] holds every read outstanding with that ID, oldest first. It is a queue per ID, not one slot, because AXI allows several transactions with the same ID in flight; they must complete in order, so the oldest entry is always the one a response with that ID answers. A single slot per ID would overwrite the first read when the second was issued.

    The address thread: get, record, issue

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // snippet: axi_rd_driver, continued
      task addr_thread();
        forever begin
          axi_rd_item req;
          seq_item_port.get(req);                // take the item: no item_done to call later
          while (n_pending >= max_outstanding) @(posedge vif.clk);   // respect the DUT's limit
          pending[req.id].push_back(req);        // clip the ticket to the rail first
          n_pending++;
          @(posedge vif.clk);
          vif.arid    <= req.id;
          vif.araddr  <= req.addr;
          vif.arvalid <= 1'b1;
          do @(posedge vif.clk); while (!vif.arready);
          vif.arvalid <= 1'b0;
        end
      endtask

    get() is get_next_item() and item_done() in one call. The sequence's finish_item returns as soon as the driver has the item, so the sequence can send the next read straight away: that is what lets several reads be outstanding at once. With get_next_item you would instead call item_done() right after the address handshake; the effect is the same. What you must never do is call item_done after get: the item is already done, and the extra call is a fatal error (SQRBADITMDN, item_done with no outstanding request).

    The max_outstanding wait is the driver honouring the DUT's (or the protocol configuration's) limit. Recording the entry before the address is driven means a fast slave can never answer a read the table does not know yet.

    The data thread: match by ID, route the response

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // snippet: axi_rd_driver, continued
      task data_thread();
        forever begin
          @(posedge vif.clk);
          if (vif.rvalid && vif.rready) begin
            axi_rd_item req, rsp;
            int id = vif.rid;
            if (!pending.exists(id) || pending[id].size() == 0) begin
              `uvm_error("R_UNEXP", $sformatf("read data for id=%0d with no read outstanding", id))
              continue;
            end
            req = pending[id].pop_front();         // same ID: the oldest read is the one answered
            n_pending--;
            rsp = axi_rd_item::type_id::create("rsp");
            rsp.set_id_info(req);                  // route the answer to the right sequence
            rsp.id = req.id; rsp.addr = req.addr; rsp.rdata = vif.rdata;
            seq_item_port.put_response(rsp);
          end
        end
      endtask
    endclass

    set_id_info(req) copies the request's sequence ID and transaction ID into the response. The sequencer uses the sequence ID to find which running sequence the response belongs to, and the sequence can use the transaction ID to match a response to one particular request (get_response(rsp, tr_id)). An unexpected ID is reported, not ignored: data the driver cannot match is a DUT bug or a driver bug, and either way the test must fail.

    The sequence: send all, then collect

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // snippet: axi_rd_seq body
    bit [3:0] ids[4] = '{1, 2, 3, 1};
    for (int i = 0; i < 4; i++) begin
      axi_rd_item it = axi_rd_item::type_id::create($sformatf("rd%0d", i));
      start_item(it);
      if (!it.randomize() with { id == ids[i]; addr == 4 * i; }) `uvm_fatal("RAND", "randomize failed")
      finish_item(it);                           // returns at once: the driver used get()
    end
    repeat (4) begin
      axi_rd_item rsp;
      get_response(rsp);                         // whichever answer arrives next
      if (rsp.rdata !== (rsp.addr ^ 32'hA5A5_0000)) `uvm_error("RD", "bad read data")
    end

    Watch it run

    The scratch run with +UVM_VERBOSITY=UVM_MEDIUM (driver messages trimmed to the important ones):

    Diagram
    UVM_INFO scratch_tb.sv(216) @ 115000: uvm_test_top.aenv.drv [AR] issued id=1 addr=0c (4 outstanding)
    UVM_INFO scratch_tb.sv(235) @ 165000: uvm_test_top.aenv.drv [R] data id=3 addr=08 rdata=a5a50008
    UVM_INFO scratch_tb.sv(264) @ 165000: uvm_test_top.aenv.sqr@@s [SEQ] got response id=3 addr=08
    UVM_INFO scratch_tb.sv(264) @ 175000: uvm_test_top.aenv.sqr@@s [SEQ] got response id=2 addr=04
    UVM_INFO scratch_tb.sv(264) @ 185000: uvm_test_top.aenv.sqr@@s [SEQ] got response id=1 addr=00
    UVM_INFO scratch_tb.sv(264) @ 195000: uvm_test_top.aenv.sqr@@s [SEQ] got response id=1 addr=0c
    UVM_INFO scratch_tb.sv(303) @ 195000: uvm_test_top [PIPE] 4 of 4 reads answered correctly
    UVM_ERROR :    0
    UVM_FATAL :    0
    • All four addresses were issued before the first data came back: 4 outstanding at 115 ns.

    • Data returned 08, 04, 00, 0c: out of order across IDs, which is what the slave was built to do.

    • Address 0c had the shortest latency of all, yet it came back last. It shares id=1 with address 00, and same-ID reads complete in order, so the slave held it, and the driver's pop_front matched it to the right request.

    What about writes?

    A write driver has three channels instead of two, and the same structure: one thread per channel, tied together by a table. The rules that shape it are AXI's ordering rules, so they belong in the driver's design notes and in your interview answer:

    Channel

    Thread duty

    Ordering rule that matters

    AW (write address)

    Issue addresses, record each in the table

    Several outstanding, same-ID responses come back in order

    W (write data)

    Send each transaction's beats, in AW order

    AXI4 forbids interleaving write data of different transactions; data may lead its address

    B (write response)

    Match by BID, retire the oldest entry for that ID, put_response

    One response per transaction, after its last data beat

    Reads have one rule writes do not: read data beats of different IDs may interleave in AXI4. With bursts, the data thread therefore keeps a beat count per outstanding entry and retires an entry only on its RLAST.

    What breaks: three ways to lose a response

    The forgotten set_id_info

    Comment out rsp.set_id_info(req) and the very first response kills the run:

    Diagram
    UVM_FATAL @ 165000: uvm_test_top.aenv.sqr [SQRPUT] Driver put a response with null sequence_id

    A freshly created response has no sequence ID, so the sequencer cannot tell which sequence it belongs to. This one is loud, which makes it the easy one.

    The sequence that left early

    Now keep set_id_info but let the sequence return after sending its four reads, without collecting. The test still passes, with four warnings:

    Diagram
    UVM_WARNING @ 165000: uvm_test_top.aenv.sqr [Sequencer] Dropping response for sequence 1, sequence not found.  Probable cause: sequence exited or has been killed
    UVM_WARNING @ 175000: uvm_test_top.aenv.sqr [Sequencer] Dropping response for sequence 1, sequence not found.  Probable cause: sequence exited or has been killed

    Every read's data was thrown away and nothing checked it. In a regression where warnings are not counted as failures, that is a silent false pass. The same warning appears after a reset when the driver answers reads issued by a sequence that the test has since killed. A reset-aware pipelined driver therefore flushes its outstanding table when reset asserts (the DUT has forgotten those reads too), instead of answering for sequences that no longer exist.

    The queue that overflowed

    Each sequence's response queue holds 8 by default. A sequence that issues 16 reads before collecting any, against a driver whose max_outstanding is 16, overflows it from the ninth response: UVM 1.2 reports an error, and the IEEE 1800.2 reference library drops the response silently unless the report is switched on. Collect as you go, use a response handler, or raise the depth with set_response_queue_depth(). The details are in Responses and pipelined drivers.

    Interview

    "Design an AXI master driver that supports multiple outstanding transactions with out-of-order completion."

    A weak answer: "Fork the drive task for each item so they run in parallel." Parallel copies of one drive task fight over the same channel signals, nothing matches responses to requests, and same-ID ordering is lost.

    A strong answer: "One thread per channel. The address thread uses get(), or get_next_item with an early item_done, so the sequence can keep sending; it waits while the outstanding count is at the limit, records each request in a table keyed by ID with a queue per ID, then drives the address. The data thread matches each response by ID to the oldest entry for that ID, builds a response with set_id_info and calls put_response. Unexpected IDs are errors. Write data follows AW order with no interleaving in AXI4; read data may interleave across IDs, so I count beats per entry. On reset I flush the table. Sequences collect with get_response by transaction ID or a response handler, and I watch the default response-queue depth of 8."

    Follow-ups to expect: Why a queue per ID and not one slot? (several same-ID transactions may be outstanding and complete in order) and What happens if you forget set_id_info? (an SQRPUT fatal: the response has no sequence ID).

    Check yourself The driver uses get_next_item and calls item_done() only when the read data arrives. Does it still pipeline?

    No. The sequence's finish_item blocks until item_done, so the sequence cannot send the second read until the first is answered, and the address thread never has more than one request to issue. Call item_done() right after the address handshake (or use get()) and return the data with put_response.

    Check yourself The outstanding table is changed to axi_rd_item pending[int], one entry per ID. The sequence issues two reads with id=1. What happens?

    The second read overwrites the first in the table. When the first read's data returns, it is matched to the second request: wrong address, wrong data check, wrong sequence transaction ID. The second read's data then finds no entry and is reported as unexpected. Same-ID transactions need a queue per ID.

    Check yourself After a mid-test reset, the log fills with "Dropping response for sequence N, sequence not found". The DUT is fine. What is the driver doing wrong?

    It is still answering reads that were outstanding before the reset, for a sequence the test killed. After reset the DUT has forgotten those reads, so the driver should flush its outstanding table when reset asserts (and its data thread should ignore the bus during reset), not put responses for them.

    Next: with slaves, resets, interrupts and pipelines covered, the agents are complete. Environment & Test assembles them into envs and reuses the block env in a two-DMA subsystem.

    Related topics

    • Responses and pipelined drivers (the sequence side)

    • Protocol agent patterns

    • Reset on the fly

    • Environment & Test

    Previous

    114 / 280

    Next: Environment & Test Classes

    Up next

    Up nextEnvironment & Test Classes

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Reactive Slave Agents
    • Reset on the Fly