Part 4 · TLM & Analysis · Intermediate

uvm_tlm_fifo Basics: Blocking put/get and Queue Semantics

Core uvm_tlm_fifo behavior: constructor/depth, put/get/peek/try_get, and how blocking semantics model producer-consumer synchronization.

What uvm_tlm_fifo gives you

A uvm_tlm_fifo #(T) is a transaction queue with standardized TLM semantics. Producers put transactions, consumers get them later, and the FIFO enforces ordering while optionally applying capacity limits.

The queue is temporal decoupling : producer and consumer no longer need to run in lockstep. This is critical whenever producer rate is bursty or consumer processing is variable.

systemverilog
class fifo_demo_comp extends uvm_component;
  `uvm_component_utils(fifo_demo_comp)

  uvm_tlm_fifo #(bus_txn) req_fifo;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    // depth=8 means producer blocks on put() when 8 items are queued
    req_fifo = new("req_fifo", this, 8);
  endfunction
endclass
diagram
[TLM] FIFO ordering guarantee

  put(txn0) -> put(txn1) -> put(txn2)
       │          │            │
       ▼          ▼            ▼
    queue[0]   queue[1]     queue[2]
       │
       ▼
  get() returns txn0, then txn1, then txn2

FIFO preserves enqueue order for a single queue instance.

Blocking operations

The most common API pair is put() and get(), both blocking task calls. put() waits when full; get() waits when empty.

systemverilog
task producer_thread();
  bus_txn t;
  forever begin
    t = bus_txn::type_id::create("t");
    assert(t.randomize());
    req_fifo.put(t);  // blocks if FIFO is full
    `uvm_info("PROD", $sformatf("enqueued id=%0d used=%0d",
             t.id, req_fifo.used()), UVM_MEDIUM)
  end
endtask

task consumer_thread();
  bus_txn t;
  forever begin
    req_fifo.get(t);  // blocks if FIFO is empty
    `uvm_info("CONS", $sformatf("dequeued id=%0d used=%0d",
             t.id, req_fifo.used()), UVM_MEDIUM)
    process_txn(t);
  end
endtask
diagram
[TLM] blocking behavior timeline

TIME -------------------------------------------------------------------->

producer:   put(A)  put(B)  put(C) [blocks full] ............ resumes put(D)
queue used:   1       2       3        3 (max depth=3)              3
consumer:  ............ get(A) get(B) get(C) ................. get(D)

When queue is full, producer naturally back-pressures.
When queue is empty, consumer naturally waits.

Non-blocking helper methods

  • try_put(t) returns 0 immediately if queue is full.

  • try_get(t) returns 0 immediately if queue is empty.

  • can_put() and can_get() support polling-style loops.

  • peek(t) reads front item without removing it.

  • used() reports current occupancy; size() reports capacity.


Minimal producer-consumer example

This skeleton shows one producer and one consumer running in parallel threads with the FIFO as synchronization boundary.

systemverilog
class pc_env extends uvm_env;
  `uvm_component_utils(pc_env)

  producer_comp prod;
  consumer_comp cons;
  uvm_tlm_fifo #(bus_txn) fifo;

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    prod = producer_comp::type_id::create("prod", this);
    cons = consumer_comp::type_id::create("cons", this);
    fifo = new("fifo", this, 16);

    // pass FIFO handle via config_db for simplicity
    uvm_config_db#(uvm_tlm_fifo#(bus_txn))::set(this, "prod", "fifo_h", fifo);
    uvm_config_db#(uvm_tlm_fifo#(bus_txn))::set(this, "cons", "fifo_h", fifo);
  endfunction
endclass

class producer_comp extends uvm_component;
  `uvm_component_utils(producer_comp)
  uvm_tlm_fifo #(bus_txn) fifo_h;

  function void build_phase(uvm_phase phase);
    if (!uvm_config_db#(uvm_tlm_fifo#(bus_txn))::get(this, "", "fifo_h", fifo_h))
      `uvm_fatal("CFG", "missing fifo_h")
  endfunction

  task run_phase(uvm_phase phase);
    repeat (100) begin
      bus_txn t = bus_txn::type_id::create("t");
      assert(t.randomize());
      fifo_h.put(t);
    end
  endtask
endclass

class consumer_comp extends uvm_component;
  `uvm_component_utils(consumer_comp)
  uvm_tlm_fifo #(bus_txn) fifo_h;

  function void build_phase(uvm_phase phase);
    if (!uvm_config_db#(uvm_tlm_fifo#(bus_txn))::get(this, "", "fifo_h", fifo_h))
      `uvm_fatal("CFG", "missing fifo_h")
  endfunction

  task run_phase(uvm_phase phase);
    bus_txn t;
    forever begin
      fifo_h.get(t);
      process_txn(t);
    end
  endtask
endclass
diagram
[TLM] behavior summary

  producer speed > consumer speed:
    queue occupancy rises toward depth
    producer eventually blocks at put()

  consumer speed > producer speed:
    queue occupancy frequently 0
    consumer blocks at get()

Either way, no busy-wait is required.

Key takeaways

  • uvm_tlm_fifo gives ordered buffering with natural blocking semantics.

  • put/get model backpressure and waiting without custom synchronization code.

  • used()/size() should be part of your observability strategy.

Common pitfalls

  • Using try_get in a tight forever loop and burning simulation time.

  • Forgetting capacity implications and assuming put never blocks.

  • Treating FIFO as transport but forgetting to preserve transaction immutability policy.