Skip to content

UVM

uvm_tlm_fifo

FIFO architecture, producer-consumer decoupling, uvm_tlm_analysis_fifo, sizing, complete patterns.

UVM Fundamentals · Module 14

Why Buffering Is Needed

TLM 1.0 put() is blocking: the producer waits until the consumer is ready. Analysis write() is non-blocking but cannot guarantee the consumer has processed the transaction. Neither handles a mismatch in production rate between producer and consumer.

ScenarioProblem Without FIFOFIFO Solution
Monitor fires fast, scoreboard checks slowlyWith put(): monitor stalls waiting for scoreboard. With write(): scoreboard may miss items if it hasn't finished the previous one.Monitor puts into FIFO instantly. Scoreboard pulls when ready. Buffer absorbs the burst.
Multiple producers, one consumerEach producer blocks waiting for the consumer — they cannot produce in parallel.Each producer puts into its own FIFO. Consumer pulls from each in round-robin.
Decoupling analysis from processingAnalysis write() is synchronous — heavy scoreboard logic runs inline during write(), delaying the monitor.write() puts into FIFO (instant). Scoreboard task pulls and processes asynchronously.

uvm_tlm_fifo Architecture — Built-In Ports

uvm_tlm_fifo #(T) is a uvm_component with built-in TLM ports on both sides. You do not implement any methods — you just connect your components to the FIFO's existing ports.

uvm_tlm_fifo architecture
uvm_tlm_fifo architecture

Figure 1 — uvm_tlm_fifo internal structure. The producer connects its put_port to put_export. The consumer connects its get_port to get_peek_export. put_ap and get_ap broadcast notifications on each operation.

Port / ExportTypeConnected ByPurpose
put_exportuvm_put_exportProducer's put_portReceives put() calls and adds to internal queue
get_peek_exportuvm_get_peek_exportConsumer's get_portReturns items from queue on get() / peek() calls
put_apuvm_analysis_portOptional — subscribe for notificationsBroadcasts write(txn) every time an item enters the FIFO
get_apuvm_analysis_portOptional — subscribe for notificationsBroadcasts write(txn) every time an item leaves the FIFO

The Producer-Consumer Pattern — Full Working Example

SystemVerilog — producer, FIFO, consumer complete wiring
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Transaction type ──────────────────────────────────────────────────
class my_txn extends uvm_sequence_item;
    `uvm_object_utils(my_txn)
    rand bit [7:0] data;
    rand bit       parity;
    function new(string name = "my_txn"); super.new(name); endfunction
endclass
 
// ── Producer ──────────────────────────────────────────────────────────
class my_producer extends uvm_component;
    `uvm_component_utils(my_producer)
    uvm_blocking_put_port #(my_txn) put_port;
 
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
 
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        put_port = new("put_port", this);
    endfunction
 
    task run_phase(uvm_phase phase);
        phase.raise_objection(this);
        repeat (5) begin
            my_txn t = my_txn::type_id::create("t");
            void'(t.randomize());
            `uvm_info("PROD", $sformatf("Putting data=0x%0h", t.data), UVM_LOW)
            put_port.put(t);   // blocks if FIFO is full
            #10;
        end
        phase.drop_objection(this);
    endtask
endclass
 
// ── Consumer ──────────────────────────────────────────────────────────
class my_consumer extends uvm_component;
    `uvm_component_utils(my_consumer)
    uvm_blocking_get_port #(my_txn) get_port;
 
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
 
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        get_port = new("get_port", this);
    endfunction
 
    task run_phase(uvm_phase phase);
        my_txn t;
        forever begin
            get_port.get(t);   // blocks until FIFO has an item
            #30;               // consumer is 3× slower — FIFO absorbs the difference
            `uvm_info("CONS", $sformatf("Got data=0x%0h", t.data), UVM_LOW)
        end
    endtask
endclass
 
// ── Environment: instantiate FIFO and connect ─────────────────────────
class my_env extends uvm_env;
    `uvm_component_utils(my_env)
    my_producer                   prod;
    my_consumer                   cons;
    uvm_tlm_fifo #(my_txn)        fifo;
 
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
 
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        prod = my_producer::type_id::create("prod", this);
        cons = my_consumer::type_id::create("cons", this);
        // FIFO: size=4 means max 4 items; size=0 means unlimited
        fifo = new("fifo", this, 4);
    endfunction
 
    function void connect_phase(uvm_phase phase);
        super.connect_phase(phase);
        prod.put_port.connect(fifo.put_export);
        cons.get_port.connect(fifo.get_peek_export);
    endfunction
endclass

uvm_tlm_analysis_fifo — Bridging Analysis and TLM Pull

The uvm_tlm_analysis_fifo is a specialised FIFO that has an analysis imp on its input side and a TLM get_peek_export on its output side. (An imp implements the interface rather than merely forwarding it — see TLM ports and exports for the distinction between a port, an export and an imp.) It bridges the fire-and-forget analysis world with the pull-based consumer world.

This is the canonical pattern for connecting a monitor (which uses an analysis port) to a scoreboard that prefers to process transactions one at a time using get(). The port/export/imp roles it relies on are covered in TLM ports and exports.

SystemVerilog — uvm_tlm_analysis_fifo connecting monitor to scoreboard
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── uvm_tlm_analysis_fifo: has analysis_export (input) + get port (output)
//
//   monitor.analysis_port  ──→  analysis_fifo.analysis_export
//                                (buffers transactions)
//                               analysis_fifo.get_peek_export  ←── scoreboard.get_port
 
class my_env extends uvm_env;
    `uvm_component_utils(my_env)
 
    apb_monitor                          mon;
    my_scoreboard                        scb;
    uvm_tlm_analysis_fifo #(apb_txn)    ap_fifo;
 
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        mon     = apb_monitor::type_id::create("mon", this);
        scb     = my_scoreboard::type_id::create("scb", this);
        // uvm_tlm_analysis_fifo is ALWAYS unbounded. Its constructor passes 0
        // to uvm_tlm_fifo::new regardless of any size you supply, so
        //     new("ap_fifo", this, 8)
        // does not give you an 8-deep FIFO - it gives you an unbounded one.
        // That is deliberate: write() is a void function and cannot block, so
        // a bounded analysis FIFO could only cope with fullness by dropping.
        ap_fifo = new("ap_fifo", this);
    endfunction
 
    function void connect_phase(uvm_phase phase);
        super.connect_phase(phase);
        // Monitor's analysis_port writes into the FIFO via its analysis_export
        mon.analysis_port.connect(ap_fifo.analysis_export);
        // Scoreboard pulls from the FIFO at its own pace
        scb.get_port.connect(ap_fifo.get_peek_export);
    endfunction
endclass
 
// ── Scoreboard now uses get_port instead of analysis_imp ──────────────
class my_scoreboard extends uvm_scoreboard;
    `uvm_component_utils(my_scoreboard)
    uvm_blocking_get_port #(apb_txn) get_port;
 
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        get_port = new("get_port", this);
    endfunction
 
    task run_phase(uvm_phase phase);
        apb_txn txn;
        forever begin
            get_port.get(txn);        // blocking — waits if FIFO is empty
            check_transaction(txn);   // can take as long as needed
        end
    endtask
    task check_transaction(apb_txn txn); /* ... */ endtask
endclass

FIFO Sizing and the Control API

SystemVerilog — FIFO sizing, query, and control methods
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Construction with size ────────────────────────────────────────────
// new(name, parent, size)
//   size = 0  : UNLIMITED — never blocks the producer on put()
//   size = N  : bounded — put() blocks when N items are already buffered
 
uvm_tlm_fifo #(my_txn) fifo_unlimited = new("fifo", this, 0);   // default: unlimited
uvm_tlm_fifo #(my_txn) fifo_bounded   = new("fifo", this, 8);   // max 8 items
 
// ── Query methods (call anywhere after build) ─────────────────────────
int  sz  = fifo.size();       // configured capacity (0 = unlimited)
int  used = fifo.used();      // current number of items in the FIFO
bit  emp = fifo.is_empty();   // 1 if no items
bit  ful = fifo.is_full();    // 1 if at capacity (always 0 for unlimited)
 
// ── flush() — drain all items ─────────────────────────────────────────
fifo.flush();   // removes all items — useful in reset sequences
 
// ── Monitoring FIFO depth during simulation ───────────────────────────
function void check_phase(uvm_phase phase);
    if (!ap_fifo.is_empty()) begin
        `uvm_error("SCB", $sformatf(
            "FIFO has %0d unprocessed transactions at end of simulation",
            ap_fifo.used()))
    end
endfunction
 
// ── peek() — read without consuming ───────────────────────────────────
uvm_blocking_peek_port #(my_txn) peek_port;
// peek_port.connect(fifo.get_peek_export);
// peek_port.peek(txn); → txn is populated but item stays in FIFO
// Useful for look-ahead logic without consuming the item

Ready-to-Run Simulator Example

Copy the code below into a single file tlm_fifo_demo.sv and run it with the commands shown. It demonstrates a 3× speed mismatch between producer and consumer, buffered by a 4-deep FIFO. Ready to Run — Questa / VCS / Xcelium

SystemVerilog — tlm_fifo_demo.sv (complete, copy and run)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// tlm_fifo_demo.sv — complete ready-to-run UVM TLM FIFO demonstration
// Compile and simulate:
//   Questa : vlog -sv tlm_fifo_demo.sv && vsim -c tlm_fifo_demo_top -do "run -all; quit"
//   VCS    : vcs -sverilog -ntb_opts uvm tlm_fifo_demo.sv && ./simv
//   Xcelium: xrun -sv -uvm tlm_fifo_demo.sv
 
`include "uvm_macros.svh"
import uvm_pkg::*;
 
// ═══════════════════════════════════════════════════════════════════════
//  TRANSACTION
// ═══════════════════════════════════════════════════════════════════════
class pkt extends uvm_sequence_item;
    `uvm_object_utils(pkt)
    rand bit [7:0] data;
    rand bit [3:0] id;
    function new(string name = "pkt"); super.new(name); endfunction
    function string convert2string();
        return $sformatf("id=%0d data=0x%0h", id, data);
    endfunction
endclass
 
// ═══════════════════════════════════════════════════════════════════════
//  PRODUCER — generates 8 packets every 10ns
// ═══════════════════════════════════════════════════════════════════════
class fast_producer extends uvm_component;
    `uvm_component_utils(fast_producer)
    uvm_blocking_put_port #(pkt) put_port;
    int num_pkts = 8;
 
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
 
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        put_port = new("put_port", this);
    endfunction
 
    task run_phase(uvm_phase phase);
        pkt p;
        phase.raise_objection(this);
        for (int i = 0; i < num_pkts; i++) begin
            p    = pkt::type_id::create($sformatf("p%0d", i));
            p.id   = i;
            p.data = $urandom_range(0, 255);
            `uvm_info("PROD", $sformatf("PUT  @ %0t%s", $time, p.convert2string()), UVM_LOW)
            put_port.put(p);   // will block if FIFO is full (size=4)
            #10;
        end
        phase.drop_objection(this);
    endtask
endclass
 
// ═══════════════════════════════════════════════════════════════════════
//  CONSUMER — processes one packet every 30ns (3× slower)
// ═══════════════════════════════════════════════════════════════════════
class slow_consumer extends uvm_component;
    `uvm_component_utils(slow_consumer)
    uvm_blocking_get_port #(pkt) get_port;
    int processed = 0;
 
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
 
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        get_port = new("get_port", this);
    endfunction
 
    task run_phase(uvm_phase phase);
        pkt p;
        forever begin
            get_port.get(p);   // blocks until FIFO has something
            #30;               // simulate slow processing
            processed++;
            `uvm_info("CONS", $sformatf("GOT  @ %0t%s  [total=%0d]",
                                    $time, p.convert2string(), processed), UVM_LOW)
        end
    endtask
endclass
 
// ═══════════════════════════════════════════════════════════════════════
//  ENVIRONMENT
// ═══════════════════════════════════════════════════════════════════════
class demo_env extends uvm_env;
    `uvm_component_utils(demo_env)
    fast_producer              prod;
    slow_consumer              cons;
    uvm_tlm_fifo #(pkt)        fifo;
 
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
 
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        prod = fast_producer::type_id::create("prod", this);
        cons = slow_consumer::type_id::create("cons", this);
        fifo = new("fifo", this, 4);   // capacity = 4 packets
    endfunction
 
    function void connect_phase(uvm_phase phase);
        super.connect_phase(phase);
        prod.put_port.connect(fifo.put_export);
        cons.get_port.connect(fifo.get_peek_export);
    endfunction
 
    function void check_phase(uvm_phase phase);
        if (fifo.used() != 0)
            `uvm_error("ENV", $sformatf("FIFO not empty at end: %0d items remain", fifo.used()))
        else
            `uvm_info("ENV", "All packets processed — FIFO empty ✓", UVM_LOW)
    endfunction
endclass
 
// ═══════════════════════════════════════════════════════════════════════
//  TEST
// ═══════════════════════════════════════════════════════════════════════
class fifo_demo_test extends uvm_test;
    `uvm_component_utils(fifo_demo_test)
    demo_env env;
 
    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction
 
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        env = demo_env::type_id::create("env", this);
    endfunction
 
    function void end_of_elaboration_phase(uvm_phase phase);
        `uvm_info("TEST", "Producer: 10ns/pkt   Consumer: 30ns/pkt   FIFO size: 4", UVM_LOW)
    endfunction
endclass
 
// ═══════════════════════════════════════════════════════════════════════
//  TOP MODULE
// ═══════════════════════════════════════════════════════════════════════
module tlm_fifo_demo_top;
    initial run_test("fifo_demo_test");
endmodule
 
// ═══════════════════════════════════════════════════════════════════════
//  EXPECTED OUTPUT (abridged):
//
//  UVM_INFO TEST: Producer: 10ns/pkt   Consumer: 30ns/pkt   FIFO size: 4
//  UVM_INFO PROD: PUT  @ 0 → id=0 data=0xA2
//  UVM_INFO PROD: PUT  @ 10 → id=1 data=0x3F
//  UVM_INFO PROD: PUT  @ 20 → id=2 data=0xC8
//  UVM_INFO PROD: PUT  @ 30 → id=3 data=0x15    ← FIFO fills; producer now stalls
//  UVM_INFO CONS: GOT  @ 30 → id=0 data=0xA2  [total=1]
//  UVM_INFO PROD: PUT  @ 30 → id=4 data=0xE1   ← producer unblocked
//  UVM_INFO CONS: GOT  @ 60 → id=1 data=0x3F  [total=2]
//  ... (consumer drains FIFO after producer finishes)
//  UVM_INFO ENV:  All packets processed — FIFO empty ✓
// ═══════════════════════════════════════════════════════════════════════

Common Patterns and Pitfalls

Pattern: Dual-FIFO Scoreboard (Request + Response)

The pattern below reads naturally and deadlocks against a pipelined DUT if the FIFOs are bounded — the mechanism is dissected in the Debug Lab further down. Compare it with the scoreboard's role and the sequence item it transports.

SystemVerilog — dual analysis_fifo: separate req and rsp paths
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Scoreboard receives separately from two monitors ──────────────────
class dual_fifo_scoreboard extends uvm_scoreboard;
    `uvm_component_utils(dual_fifo_scoreboard)
 
    uvm_blocking_get_port #(apb_req_txn) req_get_port;
    uvm_blocking_get_port #(apb_rsp_txn) rsp_get_port;
 
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        req_get_port = new("req_get_port", this);
        rsp_get_port = new("rsp_get_port", this);
    endfunction
 
    task run_phase(uvm_phase phase);
        apb_req_txn req;
        apb_rsp_txn rsp;
        forever begin
            req_get_port.get(req);   // get from req FIFO
            rsp_get_port.get(rsp);   // get matching response
            compare(req, rsp);
        end
    endtask
    task compare(apb_req_txn req, apb_rsp_txn rsp); /* ... */ endtask
endclass
 
// ── Environment wiring ────────────────────────────────────────────────
uvm_tlm_analysis_fifo #(apb_req_txn) req_fifo;
uvm_tlm_analysis_fifo #(apb_rsp_txn) rsp_fifo;
// connect_phase:
req_mon.ap.connect(req_fifo.analysis_export);
rsp_mon.ap.connect(rsp_fifo.analysis_export);
scb.req_get_port.connect(req_fifo.get_peek_export);
scb.rsp_get_port.connect(rsp_fifo.get_peek_export);

Pattern: FIFO with Depth Monitoring

SystemVerilog — monitoring FIFO depth with put_ap / get_ap
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// FIFO's put_ap fires on every put() — use it to monitor FIFO depth
class fifo_monitor extends uvm_component;
    `uvm_component_utils(fifo_monitor)
    uvm_analysis_imp #(my_txn, fifo_monitor) put_imp;
    int depth = 0;
    int max_depth = 0;
 
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        put_imp = new("put_imp", this);
    endfunction
 
    function void write(my_txn t);
        depth++;
        if (depth > max_depth) max_depth = depth;
        `uvm_info("FIFO_MON", $sformatf("depth=%0d max=%0d", depth, max_depth), UVM_HIGH)
    endfunction
endclass
 
// Connect: fifo.put_ap.connect(fifo_mon.put_imp);
// fifo.get_ap.connect(fifo_mon.get_imp); // separate imp for get side
 
// ── Common Pitfalls ───────────────────────────────────────────────────
//
// PITFALL 1: Using flush() during run_phase while consumer is waiting
//   → Consumer's get() may hang indefinitely after flush()
//   Fix: Only flush() when you know the consumer is not blocked in get()
//
// PITFALL 2: Bounded FIFO + fast producer causes deadlock
//   → Producer's put() blocks waiting for consumer to get()
//   → If consumer never runs (wrong phasing), simulation hangs
//   Fix: Use size=0 (unlimited) unless you specifically need backpressure
//
// PITFALL 3: Forgetting to check is_empty() in check_phase
//   → Missed transactions left in FIFO — silent failure
//   Fix: Always check fifo.is_empty() in check_phase and error if not

The Blocking Contract — and How It Deadlocks

Every method on a uvm_tlm_fifo falls into one of three categories, and knowing which is which is most of what prevents a hung simulation.

The FIFO's put side blocks when full and its get side blocks when empty, while try_ variants and the analysis write never blockProducerput() / try_put()uvm_tlm_fifo #(T)bounded: size N, or unbounded:size 0Consumerget() / peek() / try_get()FULL → put() suspendstry_put() returns 0 insteadEMPTY → get() suspendstry_get() returns 0 insteadanalysis_fifo: write()void, cannot block → alwaysunboundedputgettry_put, resultdiscarded12
Figure 2 — the uvm_tlm_fifo interface and which calls can block. On the input side, a blocking put_export accepts put(), which suspends the caller while the FIFO is full; try_put() never suspends and instead returns 0. On the output side, a blocking get_peek_export accepts get() and peek(), which suspend while the FIFO is empty; try_get() and try_peek() return 0 instead. A uvm_tlm_analysis_fifo replaces the put side with an analysis imp whose write() is a void function — it cannot suspend and cannot report failure, which is why that FIFO is always unbounded. The two suspending directions are the two ways an environment hangs: a producer stuck in put() on a full FIFO, and a consumer stuck in get() on an empty one.

The consequence worth stating plainly: put() on a bounded FIFO is a blocking call whose duration is controlled by the consumer. If the consumer stops consuming, the producer stops producing, and if the producer is a monitor, the environment stops observing the DUT. A monitor that is blocked in put() is not merely slow — it is not sampling the bus at all.

1

A scoreboard stopped checking halfway through the test and nothing reported an error

TLM-BLOCKING-DEADLOCK
Symptom

A request/response scoreboard verified the first few hundred transactions of every test correctly and then silently stopped. The test still ran to completion, UVM_ERROR was zero, and the run was reported as passing — but a coverage review showed the scoreboard's compare count flat-lining partway through, while the DUT kept transacting for the rest of the test.

On longer tests the simulation instead hung at end of test, with an objection that never dropped and a run_phase that never returned.

Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Env: bounded FIFOs, chosen "to catch runaway memory growth".
req_fifo = new("req_fifo", this, 8);
rsp_fifo = new("rsp_fifo", this, 8);
 
// Scoreboard: strict alternation, one request then one response.
task run_phase(uvm_phase phase);
  apb_req_txn req; apb_rsp_txn rsp;
  forever begin
    req_get_port.get(req);   // blocks until a request is available
    rsp_get_port.get(rsp);   // blocks until a response is available
    compare(req, rsp);
  end
endtask
Diagnostic Evidence

Two observations located it without needing a waveform.

Polling the FIFO depths from a background process showed req_fifo.used() climbing to 8 and staying there, while rsp_fifo.used() sat at 0. A FIFO pinned at its capacity means the producer side is suspended in put().

Confirming that took one line: the request monitor's transaction count stopped incrementing at exactly the point the scoreboard's compare count stopped. The monitor had not crashed and had not finished — it was suspended inside analysis_port-driven put() on a full FIFO, so it was no longer sampling the interface. The DUT carried on unobserved.

Root Cause

The scoreboard's strict alternation assumed request and response arrive in lockstep. The DUT is pipelined and can have many requests outstanding before the first response returns.

The sequence is deterministic once you see it. The scoreboard gets request 1 and then blocks in get(rsp) waiting for response 1. Meanwhile requests 2 through 9 arrive and fill the 8-deep request FIFO. Request 10's put() suspends the monitor. If the DUT's response depends on anything the monitor's continued operation affects — or simply if the response monitor shares a process with the request monitor — nothing ever completes, and the two sides wait on each other forever.

The bound made it a deadlock rather than a leak. With an unbounded FIFO the requests would have accumulated and the scoreboard would have drained them once responses started arriving; the design would have been memory-hungry but functionally correct. Choosing a bound to "catch runaway growth" converted a performance concern into a correctness failure, and did so in a way that reports success.

Fix

Two changes, and both matter.

Unbound the observation path. A FIFO on a monitor's output exists to decouple observation from checking, and bounding it re-couples them:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
req_fifo = new("req_fifo", this, 0);   // 0 = unbounded
rsp_fifo = new("rsp_fifo", this, 0);

Stop serialising the two streams. Even unbounded, strict alternation mis-pairs whenever responses are reordered. Consume each stream in its own process and match explicitly:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
task run_phase(uvm_phase phase);
  fork
    forever begin apb_req_txn r; req_get_port.get(r); outstanding[r.id] = r; end
    forever begin
      apb_rsp_txn s; rsp_get_port.get(s);
      if (!outstanding.exists(s.id))
        `uvm_error("SCB", $sformatf("response for unknown id %0d", s.id))
      else begin compare(outstanding[s.id], s); outstanding.delete(s.id); end
    end
  join
endtask

Verifying the fix needs a test that creates outstanding depth greater than the old bound — which is exactly the test the original environment never ran. And the durable guard is the one that would have turned this silent pass into a loud failure in the first place:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
function void check_phase(uvm_phase phase);
  super.check_phase(phase);
  if (!req_fifo.is_empty())
    `uvm_error("SCB", $sformatf("%0d requests never checked", req_fifo.used()))
  if (outstanding.size() != 0)
    `uvm_error("SCB", $sformatf("%0d requests never got a response",
                                outstanding.size()))
endfunction

An end-of-test drain check is cheap and it converts the entire class of "scoreboard quietly stopped" failures into errors. A scoreboard that finishes with items still queued did not verify them, and only the scoreboard knows that — see analysis ports for the broadcast side of this path and why scoreboards exist for what is lost when one stops checking.

Interview Questions

Decoupling in time. An analysis port is fire-and-forget: write() is a void function that runs to completion immediately in the caller's process, so every subscriber must handle the transaction the instant it is broadcast. That is fine for a coverage collector, which just samples, and awkward for a scoreboard that wants to process items one at a time at its own pace.

A uvm_tlm_fifo inserts a buffer. The producer deposits items with put() and the consumer withdraws them with get() whenever it is ready, so a slow consumer no longer forces the producer to wait — up to the FIFO's capacity. That is what makes the pull-based scoreboard style possible: a forever loop that calls get() and blocks naturally when there is nothing to do, instead of a write() callback that must return immediately.

The uvm_tlm_analysis_fifo is the adapter between the two worlds: an analysis imp on the input side so a monitor's analysis port can connect to it directly, and a get_peek_export on the output side so a scoreboard can pull from it.

Two directions block, and each blocks on the opposite fullness condition.

On the input side, put() suspends while the FIFO is full and resumes when a consumer removes an item. On the output side, get() and peek() suspend while the FIFO is empty and resume when a producer adds one. Those are the only two suspending behaviours, and between them they account for essentially every TLM-related hang.

Every blocking call has a non-blocking counterpart that returns a status instead of suspending: try_put(), try_get(), try_peek(), each returning 0 when the operation could not be performed. Use them when the caller has something else useful to do, and handle the 0 — a discarded try_put() return is a silently dropped transaction.

Note that an unbounded FIFO (size 0) can never be full, so put() on one never blocks. That is why unbounded is the right default for an observation path.

Because its input is an analysis write(), which is a void function. It cannot suspend and it cannot report failure, so there is no correct behaviour available to it when the FIFO is full — it could only drop the transaction silently. Making the FIFO unbounded removes the situation entirely.

The implementation enforces this rather than trusting the caller: the constructor passes 0 to uvm_tlm_fifo::new regardless of the size argument it received. So new("ap_fifo", this, 8) compiles, runs, and gives you an unbounded FIFO — the 8 is accepted and discarded. Anyone who passes a size expecting back-pressure gets none, and anyone who passes a size expecting to catch runaway growth gets no protection either.

The practical consequence is that memory growth on an analysis FIFO is a real risk and must be handled by making sure something drains it, not by bounding it. A check_phase that errors when the FIFO is non-empty at end of test is the standard guard, because a FIFO that is still full at the end is a FIFO whose contents were never checked.

get() removes the item from the FIFO; peek() returns a copy of the item at the head and leaves it in place. Both block while the FIFO is empty, and both are served by the same get_peek_export.

peek() earns its keep whenever the decision to consume depends on the item's contents. A scoreboard that must route transactions to different comparison paths by type or ID can peek, decide, and only then get — without the awkwardness of having already removed an item it turns out it cannot handle yet. The same applies to look-ahead logic that needs to know what is coming without committing to processing it.

The failure mode to watch for is a peek() in a forever loop with no matching get() on some path. Because peek leaves the item in place, the loop spins on the same transaction indefinitely, which presents as a simulation that makes no progress while consuming full CPU — quite different from the suspended-process hang that a missing put() consumer produces, and worth being able to tell apart.

A blocking get()/put() pair on a bounded FIFO, with nothing that notices the stall.

The usual shape is a scoreboard that serialises two streams — get a request, then get its response — against a pipelined DUT that has many requests outstanding. The scoreboard blocks waiting for the first response while further requests accumulate; the request FIFO reaches its bound; and the monitor's put() suspends. From that moment the monitor is no longer sampling the interface at all, so the DUT transacts unobserved and the scoreboard compares nothing further.

Nothing errors because nothing is designed to. No transaction was corrupted, no check failed, and no objection was raised — the checking simply stopped, and a checker that stops checking produces no output by definition.

Diagnosis is quick once suspected: poll used() on the FIFOs. One pinned at capacity while the other sits at zero identifies both the stalled direction and the stream that never arrived. The fixes are to unbound observation-path FIFOs and to consume each stream in its own process, matching by ID rather than by arrival order. The guard that turns the silence into a failure is a check_phase that errors when a FIFO is non-empty or when the outstanding-request table is not empty at end of test.

Bounding is right when the FIFO models something that is genuinely bounded in the design, and wrong when it is protecting an observation path.

The good case is a FIFO standing in for real hardware storage — a model of a DUT queue, or a stimulus path where you want the producer throttled so that back-pressure is exercised. There the bound is the point: put() blocking is the modelled behaviour, and a producer that stalls is the design's intent rather than an accident.

The bad case is a FIFO between a monitor and a scoreboard. Bounding it re-couples the two things the FIFO was inserted to decouple, and it does so in the worst direction — the monitor is the side that gets suspended, so the consequence of the scoreboard falling behind is that the environment stops observing the DUT. The instinct behind it is usually reasonable ("catch runaway memory growth"), and the result is that a performance concern has been converted into a correctness failure that reports success.

The right way to catch runaway growth on an unbounded FIFO is to watch it rather than to cap it: sample used() periodically and error past a threshold, and check that the FIFO is empty in check_phase. Both report the problem instead of causing a different one.

Where This Is Specified

  • IEEE 1800.2-2020 (UVM) — uvm_tlm_fifo. The put_export and get_peek_export, the blocking put/get/peek and non-blocking try_put/try_get/try_peek interfaces, the size/used/is_empty/is_full/flush control API, and the convention that a size of 0 means unbounded.
  • IEEE 1800.2-2020 — uvm_tlm_analysis_fifo. The analysis imp on the input side, the get_peek_export on the output, and its construction as an unbounded uvm_tlm_fifo irrespective of the size argument.
  • IEEE 1800.2-2020 — TLM 1 port/export/imp model. Which side supplies the implementation, and why an analysis write() is a void function that neither blocks nor reports failure.
  • Accellera UVM User Guide — scoreboard connectivity. The monitor-to-analysis-FIFO-to-scoreboard pattern and end-of-test drain checking.

Quick Reference

TaskCall / Declaration
Create unlimited FIFOuvm_tlm_fifo #(T) fifo = new("fifo", this, 0)
Create bounded FIFO (N items)uvm_tlm_fifo #(T) fifo = new("fifo", this, N)
Connect producerprod.put_port.connect(fifo.put_export)
Connect consumercons.get_port.connect(fifo.get_peek_export)
Analysis FIFO (monitor → scoreboard)uvm_tlm_analysis_fifo #(T)mon.ap.connect(af.analysis_export)scb.get_port.connect(af.get_peek_export)
Get item countfifo.used()
Check empty / fullfifo.is_empty() / fifo.is_full()
Flush all itemsfifo.flush()
Monitor put/get eventsfifo.put_ap.connect(mon_imp) / fifo.get_ap.connect(mon_imp)
Shell — compile and run commands for all major simulators
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
## ── Questa / ModelSim ────────────────────────────────────────────────
vlog -sv -timescale 1ns/1ps tlm_fifo_demo.sv
vsim -c tlm_fifo_demo_top -do "run -all; quit -f"
 
## With verbosity control:
vsim -c tlm_fifo_demo_top +UVM_VERBOSITY=UVM_LOW -do "run -all; quit -f"
 
## ── VCS ───────────────────────────────────────────────────────────────
vcs -sverilog -ntb_opts uvm -timescale=1ns/1ps tlm_fifo_demo.sv -o simv
./simv +UVM_TESTNAME=fifo_demo_test +UVM_VERBOSITY=UVM_LOW
 
## ── Xcelium ───────────────────────────────────────────────────────────
xrun -sv -uvm -timescale 1ns/1ps tlm_fifo_demo.sv \
     -input "run; exit" \
     +UVM_TESTNAME=fifo_demo_test +UVM_VERBOSITY=UVM_LOW
 
## ── Expected key output lines: ───────────────────────────────────────
## UVM_INFO PROD: PUT  @ 0 → id=0 data=0x??
## UVM_INFO PROD: PUT  @ 10 → id=1 data=0x??
## UVM_INFO PROD: PUT  @ 20 → id=2 data=0x??
## UVM_INFO PROD: PUT  @ 30 → id=3 data=0x??   ← FIFO full (4 items)
## UVM_INFO CONS: GOT  @ 30 → id=0 ...          ← consumer unblocks producer
## ... (all 8 packets processed)
## UVM_INFO ENV:  All packets processed — FIFO empty ✓
## UVM_INFO @ 0: UVM_ERROR :   0   UVM_FATAL :   0

§9 — Code Examples

Example 1 — Beginner: Backpressure — Bounded FIFO Slowing the Producer

The most important FIFO behaviour to understand first: when the FIFO is full, the producer's put() blocks. This is backpressure — the consumer controls how fast the producer runs. The simulation timeline shows this clearly.

SystemVerilog — backpressure: bounded FIFO stalls the producer
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Transaction ────────────────────────────────────────────────────────
class data_pkt extends uvm_sequence_item;
    `uvm_object_utils(data_pkt)
    int id;
    function new(string n="data_pkt"); super.new(n); endfunction
endclass
 
// ── Producer: puts a packet every 10ns ────────────────────────────────
task run_phase(uvm_phase phase);
    data_pkt p;
    phase.raise_objection(this);
    for (int i = 0; i < 6; i++) begin
        p    = data_pkt::type_id::create($sformatf("p%0d",i));
        p.id = i;
        `uvm_info("PROD", $sformatf("Attempting put(%0d) @ %0t", i, $time), UVM_LOW)
        put_port.put(p);   // BLOCKS if FIFO has 2 items (size=2)
        `uvm_info("PROD", $sformatf("put(%0d) returned @ %0t", i, $time), UVM_LOW)
        #10;
    end
    phase.drop_objection(this);
endtask
 
// ── Consumer: gets one packet every 40ns (4× slower) ─────────────────
task run_phase(uvm_phase phase);
    data_pkt p;
    forever begin
        get_port.get(p);  // waits for item
        #40;              // slow processing
        `uvm_info("CONS", $sformatf("Processed(%0d) @ %0t", p.id, $time), UVM_LOW)
    end
endtask
 
// ── FIFO: size=2 — forces backpressure ───────────────────────────────
uvm_tlm_fifo#(data_pkt) fifo = new("fifo", this, 2);  // max 2 items
 
// Simulation timeline (FIFO size=2, producer 10ns, consumer 40ns):
// @  0: put(0) → FIFO=[0]     — FIFO has 1 item, put returns immediately
// @  0: put(1) → FIFO=[0,1]   — FIFO has 2 items (FULL), put returns
// @ 10: put(2) → FIFO=[0,1,?] — FIFO FULL! put() BLOCKS here
// @ 40: Cons gets(0) → FIFO=[1] — put(2) unblocked! FIFO=[1,2]
// @ 50: put(3) returns immediately (FIFO had space)
// Consumer controls producer throughput — backpressure in action

Example 2 — Intermediate: Analysis FIFO Decoupling Monitor from Scoreboard

The canonical production pattern. Monitor uses analysis_port (fire-and-forget). Scoreboard uses get_port (pull-when-ready). The analysis FIFO bridges them.

SystemVerilog — analysis_fifo: monitor writes, scoreboard pulls
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Monitor: uses standard analysis_port — unchanged ──────────────────
class apb_monitor extends uvm_monitor;
    uvm_analysis_port#(apb_txn) ap;
 
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        ap = new("ap", this);
    endfunction
 
    task run_phase(uvm_phase phase);
        apb_txn txn;
        forever begin
            @(posedge vif.clk iff vif.psel);
            txn      = apb_txn::type_id::create("txn");
            txn.addr = vif.paddr;
            txn.data = vif.pwdata;
            ap.write(txn);   // non-blocking — into the FIFO instantly
        end
    endtask
    virtual apb_if vif;
endclass
 
// ── Scoreboard: uses get_port — can take as long as needed per item ────
class apb_scoreboard extends uvm_scoreboard;
    uvm_blocking_get_port#(apb_txn) get_port;
 
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        get_port = new("get_port", this);
    endfunction
 
    task run_phase(uvm_phase phase);
        apb_txn txn;
        forever begin
            get_port.get(txn);       // waits if FIFO is empty
            do_reference_model(txn);  // can be slow — doesn't affect monitor
            compare_and_report(txn);
        end
    endtask
 
    task do_reference_model(apb_txn txn); #5; endtask  // expensive
    task compare_and_report(apb_txn txn); /* ... */ endtask
endclass
 
// ── Env: the FIFO is the glue ──────────────────────────────────────────
class apb_env extends uvm_env;
    apb_monitor                         mon;
    apb_scoreboard                      scb;
    uvm_tlm_analysis_fifo#(apb_txn)   af;  // the bridge
 
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        mon = apb_monitor::type_id::create("mon", this);
        scb = apb_scoreboard::type_id::create("scb", this);
        af  = new("af", this);   // size=0: unlimited — monitor never stalls
    endfunction
 
    function void connect_phase(uvm_phase phase);
        super.connect_phase(phase);
        mon.ap.connect(af.analysis_export);     // analysis → FIFO input
        scb.get_port.connect(af.get_peek_export); // FIFO output → scoreboard
    endfunction
 
    function void check_phase(uvm_phase phase);
        if (af.used() != 0)
            `uvm_error("ENV", $sformatf(
                "Analysis FIFO has %0d unprocessed items at test end", af.used()))
    endfunction
endclass

Example 3 — Verification: peek() Before get() for Order-Sensitive Checking

peek() reads the next item without removing it. Useful when you need to inspect the next transaction before deciding whether to consume it — for example, in an out-of-order protocol where you need to scan for a specific ID.

SystemVerilog — peek() for non-destructive inspection
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Scoreboard with peek() for look-ahead ─────────────────────────────
class reorder_scoreboard extends uvm_scoreboard;
    `uvm_component_utils(reorder_scoreboard)
 
    uvm_blocking_get_port#(axi_txn)  get_port;
    uvm_blocking_peek_port#(axi_txn) peek_port;
 
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        get_port  = new("get_port",  this);
        peek_port = new("peek_port", this);
    endfunction
 
    // connect_phase:
    //   get_port.connect(fifo.get_peek_export)
    //   peek_port.connect(fifo.get_peek_export)  ← same export serves both
 
    task run_phase(uvm_phase phase);
        axi_txn  head;
        axi_txn  next;
        int      expected_id = 0;
        forever begin
            // Look at the next item WITHOUT consuming it
            peek_port.peek(head);
 
            if (head.txn_id == expected_id) begin
                // It's what we want — consume and process it
                get_port.get(next);
                process(next);
                expected_id++;
            end else begin
                // Not what we want yet — wait for the DUT to send the right one
                `uvm_info("SCB", $sformatf(
                    "Waiting: head=%0d, want=%0d", head.txn_id, expected_id),
                    UVM_HIGH)
                #10;   // wait and check again
            end
        end
    endtask
    task process(axi_txn t); /* ... */ endtask
endclass
 
// Note: get_peek_export handles both get() and peek() from the same FIFO.
// peek() reads the head without advancing the FIFO pointer.
// A subsequent get() then removes that same item.
// Never call peek() and get() on the same item from different threads — race.

Example 4 — Tricky: flush() Mid-Test Reset Sequence

When a DUT reset fires during a test, the FIFO may hold stale transactions that should be discarded. flush() clears the FIFO — but if the consumer is blocking inside get(), it will hang forever after the flush.

SystemVerilog — flush() during reset and the consumer wake-up pattern
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Reset handler in the env or test ─────────────────────────────────
task handle_reset();
    `uvm_info("ENV", "Reset detected — flushing FIFO", UVM_LOW)
 
    // PROBLEM: if scoreboard is blocking inside get_port.get()
    // and FIFO becomes empty after flush(), scoreboard hangs forever
 
    // ✓ Safe pattern: signal the scoreboard to stop before flushing
    scb.handle_reset();    // scoreboard exits its get() loop via a flag
    @(posedge vif.clk);    // wait one clock for scoreboard to unblock
    af.flush();            // now safe to flush
 
    `uvm_info("ENV", $sformatf("FIFO flushed: %0d items discarded",
        af.used()), UVM_LOW)    // used() = 0 after flush
endtask
 
// ── Scoreboard with reset-aware get() loop ────────────────────────────
class reset_aware_scoreboard extends uvm_scoreboard;
    bit in_reset = 0;
 
    function void handle_reset();
        in_reset = 1;   // signal to exit get() loop
    endfunction
 
    task run_phase(uvm_phase phase);
        apb_txn txn;
        forever begin
            if (in_reset) begin
                in_reset = 0;
                `uvm_info("SCB", "Reset acknowledged — restarting", UVM_LOW)
                continue;
            end
            // Non-blocking try_get() allows the loop to check in_reset
            if (!get_port.try_get(txn)) begin
                #1;   // poll — not ideal but avoids blocking
                continue;
            end
            process(txn);
        end
    endtask
    task process(apb_txn t); /* ... */ endtask
endclass
 
// Alternative: use try_get() instead of get() across all scoreboard loops
// to avoid permanent blocking when the FIFO goes unexpectedly empty.

§10 — Bugs & Debugging

Bug 1 — Not Checking FIFO Empty in check_phase — Silent Transaction Loss

SystemVerilog — missing FIFO drain check and the fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ WRONG — no check for unprocessed items ───────────────────────────
class bad_env extends uvm_env;
    uvm_tlm_analysis_fifo#(apb_txn) af;
    // check_phase: nothing here → FIFO may have items at simulation end
endclass
// Test ends: UVM_ERROR = 0 → CI reports PASS
// But af.used() = 3 → 3 unverified transactions silently lost
 
// ✓ CORRECT — always check FIFO in check_phase ────────────────────────
class good_env extends uvm_env;
    uvm_tlm_analysis_fifo#(apb_txn) af;
 
    function void check_phase(uvm_phase phase);
        if (af.used() != 0) begin
            `uvm_error("ENV", $sformatf(
                "%0d unprocessed transactions in analysis FIFO at test end."
                " Scoreboard may be too slow or dropped its loop early.",
                af.used()))
        end else
            `uvm_info("ENV", "Analysis FIFO drained — all transactions verified", UVM_NONE)
    endfunction
endclass
 
// Also add a timeout guard in the scoreboard's run_phase:
// Use a fork-join_any with a time limit to detect stuck consumers.

Bug 2 — Bounded FIFO Causing Simulation Hang (Deadlock)

SystemVerilog — bounded FIFO deadlock and size=0 fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ DEADLOCK — bounded FIFO, consumer starts late ─────────────────────
// env.build_phase:
uvm_tlm_fifo#(my_txn) fifo = new("fifo", this, 4);  // size=4
 
// Producer runs immediately in run_phase at T=0
// Puts items 0,1,2,3 → FIFO FULL at T=0
// Producer blocks on put(item_4)
//
// Consumer triggered by DUT interrupt — which never fires because
// the DUT is also waiting for the producer to drive more transactions.
// DEADLOCK: producer waits for consumer, consumer waits for DUT,
// DUT waits for producer. Simulation hangs.
 
// ✓ FIX — size=0 for analysis FIFOs and most verification uses ────────
uvm_tlm_analysis_fifo#(my_txn) af = new("af", this);
// size=0 (default): unlimited depth → producer NEVER blocks
// Memory usage grows if consumer is perpetually slower, but deadlock impossible
// Use bounded size ONLY when you intentionally want backpressure testing
 
// Diagnosis: if simulation hangs, enable +UVM_OBJECTION_TRACE and look for
// which component is still holding an objection.
// Then grep the log for the last PROD PUT line — the stuck producer.
// Check the FIFO depth with a periodic monitoring task.

Bug 3 — Consumer get() Loop Has No Drain — Test Ends Early

SystemVerilog — scoreboard-side objection for complete FIFO drain
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ✓ Scoreboard raises its own objection and drops it only when FIFO is empty
class draining_scoreboard extends uvm_scoreboard;
    uvm_blocking_get_port#(apb_txn) get_port;
    uvm_tlm_analysis_fifo#(apb_txn)* af;  // handle to the env's FIFO
 
    task run_phase(uvm_phase phase);
        apb_txn txn;
        phase.raise_objection(this);  // SCB holds objection
        forever begin
            get_port.get(txn);   // blocks if FIFO empty
            process(txn);
            // When producer drops its objection, the FIFO will drain.
            // Once FIFO is empty AND no more items expected, drop ours.
            if (af.is_empty() && producer_done()) break;
        end
        phase.drop_objection(this);  // only drop when truly done
    endtask
 
    function bit producer_done();
        // Check if the test's objection count has dropped to zero
        return (uvm_root::get().get_objection_count(UVM_PHASE_OBJECTION) == 1);
        // 1 = only our own objection remains
    endfunction
    task process(apb_txn t); /* ... */ endtask
endclass

§11 — Ready-to-Run: Analysis FIFO Bridge Demo

A complete, self-contained demo showing uvm_tlm_analysis_fifo bridging a fast producer (analysis write) to a slow consumer (blocking get). The check_phase verifies the FIFO is drained. Run this and observe the timing difference. Ready to Run — Questa / VCS / Xcelium

analysis_fifo_demo.sv — compile and run
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// analysis_fifo_demo.sv
// Compile: vlog -sv analysis_fifo_demo.sv
// Run:     vsim -c work.tb_af_top +UVM_TESTNAME=af_demo_test -do "run -all; quit"
 
`include "uvm_macros.svh"
import uvm_pkg::*;
 
class af_txn extends uvm_sequence_item;
    `uvm_object_utils(af_txn)
    int id;
    int data;
    function new(string n="af_txn"); super.new(n); endfunction
endclass
 
// ── Fast publisher: writes via analysis_port every 5ns ────────────────
class fast_pub extends uvm_component;
    `uvm_component_utils(fast_pub)
    uvm_analysis_port#(af_txn) ap;
 
    function new(string n, uvm_component p); super.new(n,p); endfunction
 
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        ap = new("ap", this);
    endfunction
 
    task run_phase(uvm_phase phase);
        af_txn t;
        phase.raise_objection(this);
        for (int i=0; i<6; i++) begin
            t      = af_txn::type_id::create($sformatf("t%0d",i));
            t.id   = i;
            t.data = (i+1) * 16;
            `uvm_info("PUB", $sformatf("write(%0d) @ %0t", i, $time), UVM_LOW)
            ap.write(t);   // non-blocking — into FIFO instantly
            #5;
        end
        phase.drop_objection(this);  // publisher done — 6 items in FIFO
    endtask
endclass
 
// ── Slow subscriber: pulls from FIFO every 20ns ────────────────────────
class slow_sub extends uvm_component;
    `uvm_component_utils(slow_sub)
    uvm_blocking_get_port#(af_txn) get_port;
    int total = 0;
 
    function new(string n, uvm_component p); super.new(n,p); endfunction
 
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        get_port = new("get_port", this);
    endfunction
 
    task run_phase(uvm_phase phase);
        af_txn t;
        phase.raise_objection(this);  // SUB holds objection until done
        repeat (6) begin
            get_port.get(t);  // blocks if FIFO empty
            #20;              // slow processing — 4× slower than publisher
            total++;
            `uvm_info("SUB", $sformatf(
                "got(%0d) data=%0d @ %0t [%0d/6]",
                t.id, t.data, $time, total), UVM_LOW)
        end
        phase.drop_objection(this);  // drop only after all 6 processed
    endtask
endclass
 
// ── Environment ────────────────────────────────────────────────────────
class af_env extends uvm_env;
    `uvm_component_utils(af_env)
    fast_pub                          pub;
    slow_sub                          sub;
    uvm_tlm_analysis_fifo#(af_txn)   af;
 
    function new(string n, uvm_component p); super.new(n,p); endfunction
 
    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        pub = fast_pub::type_id::create("pub", this);
        sub = slow_sub::type_id::create("sub", this);
        af  = new("af", this);   // unlimited depth
    endfunction
 
    function void connect_phase(uvm_phase phase);
        super.connect_phase(phase);
        pub.ap.connect(af.analysis_export);
        sub.get_port.connect(af.get_peek_export);
    endfunction
 
    function void check_phase(uvm_phase phase);
        if (af.used() != 0)
            `uvm_error("ENV", $sformatf("FIFO not drained: %0d items remain", af.used()))
        else
            `uvm_info("ENV", "FIFO fully drained. All items processed.", UVM_NONE)
    endfunction
endclass
 
class af_demo_test extends uvm_test;
    `uvm_component_utils(af_demo_test)
    af_env env;
    function new(string n, uvm_component p); super.new(n,p); endfunction
    function void build_phase(uvm_phase phase);
        env = af_env::type_id::create("env", this);
    endfunction
endclass
 
module tb_af_top;
    initial run_test();
endmodule
 
// Expected output:
// UVM_INFO PUB: write(0) @ 0     ← publisher fires all 6 by T=25
// UVM_INFO PUB: write(1) @ 5
// UVM_INFO PUB: write(2) @ 10
// UVM_INFO PUB: write(3) @ 15
// UVM_INFO PUB: write(4) @ 20
// UVM_INFO PUB: write(5) @ 25
// UVM_INFO SUB: got(0) data=16 @ 20  [1/6]  ← subscriber starts draining
// UVM_INFO SUB: got(1) data=32 @ 40  [2/6]
// UVM_INFO SUB: got(2) data=48 @ 60  [3/6]
// UVM_INFO SUB: got(3) data=64 @ 80  [4/6]
// UVM_INFO SUB: got(4) data=80 @ 100 [5/6]
// UVM_INFO SUB: got(5) data=96 @ 120 [6/6]
// UVM_INFO ENV: FIFO fully drained. All items processed.
//
// Note: publisher finishes at T=25, subscriber finishes at T=120.
// The FIFO absorbed the burst and let subscriber work at its own pace.

§12 — Interview Questions

Beginner Level

Intermediate Level

Senior / Architect Level

§13 — Best Practices

RulePracticeWhy It Matters
BP-1Always check fifo.used() == 0 in check_phaseTests silently PASS with unprocessed transactions in the FIFO if this check is absent
BP-2Use size=0 (unlimited) for analysis FIFOs connected to monitorsMonitors must never stall; unlimited depth prevents deadlock; use bounded size only for deliberate backpressure testing
BP-3Let the scoreboard hold its own objection until the FIFO is drainedPrevents test ending before all transactions are verified — the producer's objection drop should not terminate the scoreboard's drain
BP-4Call flush() only when the consumer is not blocking in get()flush() after get() leaves the consumer blocking forever on an empty FIFO — simulation hangs until timeout
BP-5Use uvm_tlm_analysis_fifo to decouple monitor write() from scoreboard processingScoreboard task-based processing can take time freely; monitor is never slowed by scoreboard workload
BP-6Connect both get_port and peek_port to the same get_peek_exportThe export handles both — no need for two separate FIFOs; peek() does not consume the item
BP-7Name FIFOs descriptively: req_fifo, rsp_fifo, ap_fifoWhen an env has multiple FIFOs, generic names like "fifo" cause confusion in debug logs and check_phase error messages
BP-8Subscribe to fifo.put_ap and fifo.get_ap for FIFO depth tracking in coverage or protocol checkersThese built-in analysis ports fire on every put and get — a FIFO depth monitor can track max depth as a coverage metric without modifying the FIFO or its connected components

§14 — Summary

Aspectuvm_tlm_fifouvm_tlm_analysis_fifo
Input sideput_export — accepts blocking put() callsanalysis_export — accepts analysis write() calls
Output sideget_peek_export — serves get() and peek()get_peek_export — same
Producer interfaceTLM 1.0 put_port (blocking, can stall)Analysis port (non-blocking, never stalls)
BackpressureYes — bounded FIFO blocks put() when fullNo — analysis write() is non-blocking regardless of depth
Canonical useProducer-consumer with rate control; pipeline stagesMonitor (write) → scoreboard/checker (get); bridge pattern
Size=0Unlimited depth — put() never blocksUnlimited depth — default for analysis FIFOs
Monitoringput_ap, get_ap for event notificationsSame — put_ap fires on write(), get_ap on get()

Continue learning