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.
| Scenario | Problem Without FIFO | FIFO Solution |
|---|---|---|
| Monitor fires fast, scoreboard checks slowly | With 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 consumer | Each 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 processing | Analysis 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.
PRODUCER(monitor / driver)put_portput(txn)uvm_tlm_fifo #(T)put_exportget_peek_exportInternal SystemVerilog Queue[ txn_0 | txn_1 | txn_2 | ... | txn_N ]size: 0 = unlimited · FIFO order · thread-safeput_apget_apget(txn)CONSUMER(scoreboard / checker)get_portfires on putfires on get
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 / Export | Type | Connected By | Purpose |
|---|---|---|---|
put_export | uvm_put_export | Producer's put_port | Receives put() calls and adds to internal queue |
get_peek_export | uvm_get_peek_export | Consumer's get_port | Returns items from queue on get() / peek() calls |
put_ap | uvm_analysis_port | Optional — subscribe for notifications | Broadcasts write(txn) every time an item enters the FIFO |
get_ap | uvm_analysis_port | Optional — subscribe for notifications | Broadcasts write(txn) every time an item leaves the FIFO |
The Producer-Consumer Pattern — Full Working Example
// ── 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
endclassuvm_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. 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 analysis_port) to a scoreboard that prefers to process transactions one at a time using get().
// ── 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);
ap_fifo = new("ap_fifo", this); // size=0 (unlimited) by default
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
endclassFIFO Sizing and the Control API
// ── 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 itemReady-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
// 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)
// ── 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
// 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 notQuick Reference
| Task | Call / Declaration |
|---|---|
| Create unlimited FIFO | uvm_tlm_fifo #(T) fifo = new("fifo", this, 0) |
| Create bounded FIFO (N items) | uvm_tlm_fifo #(T) fifo = new("fifo", this, N) |
| Connect producer | prod.put_port.connect(fifo.put_export) |
| Connect consumer | cons.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 count | fifo.used() |
| Check empty / full | fifo.is_empty() / fifo.is_full() |
| Flush all items | fifo.flush() |
| Monitor put/get events | fifo.put_ap.connect(mon_imp) / fifo.get_ap.connect(mon_imp) |
## ── 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.
// ── 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 actionExample 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.
// ── 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
endclassExample 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.
// ── 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.
// ── 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
⚠️ Production Bug — Test PASSes with Unprocessed Transactions in the FIFO
The scoreboard's run_phase pulls from the analysis FIFO. The test drops its objection. Simulation ends before the scoreboard has processed all items. The remaining transactions sit in the FIFO untouched — they're never checked. The test reports PASS. The DUT sent 100 transactions, only 97 were verified. Without a check_phase guard, this goes undetected for weeks.
// ❌ 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)
⚠️ Production Bug — Simulation Hangs at UVM_TIMEOUT with No Useful Stack
A 4-item bounded FIFO fills up. The producer's put() blocks, waiting for the consumer to get(). But the consumer only starts processing in a second phase event or after an external trigger that never fires. The simulation hangs until UVM_TIMEOUT. The log shows the producer's last successful put() but nothing after — it's stuck inside put(), waiting for space that never comes.
// ❌ 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
// ✓ 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: 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
| Rule | Practice | Why It Matters |
|---|---|---|
| BP-1 | Always check fifo.used() == 0 in check_phase | Tests silently PASS with unprocessed transactions in the FIFO if this check is absent |
| BP-2 | Use size=0 (unlimited) for analysis FIFOs connected to monitors | Monitors must never stall; unlimited depth prevents deadlock; use bounded size only for deliberate backpressure testing |
| BP-3 | Let the scoreboard hold its own objection until the FIFO is drained | Prevents test ending before all transactions are verified — the producer's objection drop should not terminate the scoreboard's drain |
| BP-4 | Call 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-5 | Use uvm_tlm_analysis_fifo to decouple monitor write() from scoreboard processing | Scoreboard task-based processing can take time freely; monitor is never slowed by scoreboard workload |
| BP-6 | Connect both get_port and peek_port to the same get_peek_export | The export handles both — no need for two separate FIFOs; peek() does not consume the item |
| BP-7 | Name FIFOs descriptively: req_fifo, rsp_fifo, ap_fifo | When an env has multiple FIFOs, generic names like "fifo" cause confusion in debug logs and check_phase error messages |
| BP-8 | Subscribe to fifo.put_ap and fifo.get_ap for FIFO depth tracking in coverage or protocol checkers | These 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
| Aspect | uvm_tlm_fifo | uvm_tlm_analysis_fifo |
|---|---|---|
| Input side | put_export — accepts blocking put() calls | analysis_export — accepts analysis write() calls |
| Output side | get_peek_export — serves get() and peek() | get_peek_export — same |
| Producer interface | TLM 1.0 put_port (blocking, can stall) | Analysis port (non-blocking, never stalls) |
| Backpressure | Yes — bounded FIFO blocks put() when full | No — analysis write() is non-blocking regardless of depth |
| Canonical use | Producer-consumer with rate control; pipeline stages | Monitor (write) → scoreboard/checker (get); bridge pattern |
| Size=0 | Unlimited depth — put() never blocks | Unlimited depth — default for analysis FIFOs |
| Monitoring | put_ap, get_ap for event notifications | Same — put_ap fires on write(), get_ap on get() |