Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

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

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • Start Here4
  • UVM Foundations14
  • UVM Phases & Simulation Lifecycle26
  • Factory & Configuration15
  • Transaction-Level Modeling (TLM)13
    • TLM 1: Ports, Exports and Analysis
      • Who Calls, Who Implements: Ports, Exports and Imps
      • connect() Rules: Which Way Each Link Points
      • put, get, peek and transport: Flow-Controlled TLM
      • Analysis Broadcast: One write(), Many Listeners
      • TLM1 Channels and Port Cardinality
      • Analysis Connectivity Patterns: imp_decl vs FIFOs
      • Connection Triage and FIFO Sizing: A Debug Playbook
      • The TLM Lessons' Bench File: Full Source
    • TLM 2.0 Sockets
  • UVM Sequences & Stimulus21
  • UVM Agents & Protocol VIP21
  • Environment & Test Classes11
  • Checking & Coverage in UVM21
  • UVM Register Abstraction Layer (RAL)22
  • Advanced UVM in Practice13
  • Debug & Regression10
  • Coding Practice47
  • Verification in Industry Practice10
  • Senior Prep: Architecture, Leadership and Interviews32
Loading lesson

‹TLM 1: Ports, Exports and Analysis
  1. Topics
  2. UVM Verification
  3. Transaction-Level Modeling (TLM)
  4. TLM 1: Ports, Exports and Analysis
  5. Connection Triage and FIFO Sizing: A Debug Playbook

Part 4 · TLM & Analysis · Intermediate

Connection Triage and FIFO Sizing: A Debug Playbook

A step-by-step playbook for a transaction that went missing between a monitor and a checker, publish/receive/check counters, sizing a FIFO from measured occupancy, and choosing a loss policy.

On this page

    A nightly regression reports SB_EMPTY on a handful of seeds: no words were compared. On the other seeds the same test passes with hundreds of matches. The DUT waveform shows data moving on every failing seed. Somewhere between the monitor's pins and the scoreboard's compare, transactions are disappearing. Where do you look first, and in what order, so the search takes minutes instead of a day?

    The idea: follow the transaction outward from the pins

    A transaction that the DUT really produced can be lost in exactly five places: it was never sampled, never published, published to nobody, received but never checked, or still waiting when the test ended. Each place has a cheap, specific test. Triage is just asking them in order, from the cheapest and most structural to the most dynamic, like a plumber who checks that the pipes are connected before measuring the water pressure.

    Where it can be lost

    Cheapest evidence

    Lesson

    Elaboration refused a link

    A Connection Error or BUILDERR at time 0

    connect() rules, cardinality

    Published to nobody

    debug_connected_to() says Connected implementations: none

    imp_decl vs FIFOs

    Never sampled or never published

    Monitor's published count is lower than the bus activity

    Agent architecture

    Received, never checked

    Received count above checked count; FIFO used() not 0

    This lesson

    Still queued at end of test

    Leftover or unread errors in check_phase

    End-of-test strategy

    The picture: the triage order

    Decision flow: check for time-0 errors, then the topology, then debug_connected_to, then compare published with received counts, then received with checked counts.
    Structural questions first, because each takes one line of code and rules out a whole class of bugs. Counters come last, because they need a run.

    Build it: the instrumentation that answers each question

    Structural checks at end of elaboration

    The bench already prints the topology from dma_base_test::end_of_elaboration_phase. Add the connection dump for the publisher you suspect, guarded by a plusarg so it costs nothing in normal runs:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    // snippet: in a test or env
    function void end_of_elaboration_phase(uvm_phase phase);
      super.end_of_elaboration_phase(phase);
      if ($test$plusargs("TLM_DEBUG")) begin
        env.out_agt.ap.debug_connected_to();       // who will this port call?
        env.sb.out_export.debug_provided_to();     // who feeds this imp?
      end
    endfunction

    Counters at each hand-off

    Give the monitor a published count and each checker a received and a checked count, and compare them in report_phase, where every count is final. A gap between published and received means the connection or the subscriber's filter lost items; a gap between received and checked means items arrived and were never compared.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // snippet
    // apb_monitor:  int unsigned n_published;   // n_published++ next to ap.write(it)
    // scoreboard:   int unsigned n_received;    // n_received++ first thing in write_out
    //               (n_match + n_mismatch) is the checked count
    function void report_phase(uvm_phase phase);
      int unsigned n_checked = sb.n_match + sb.n_mismatch;
      if (out_mon.n_published != sb.n_received)
        `uvm_error("TLM_AUDIT", $sformatf("published %0d, received %0d", out_mon.n_published, sb.n_received))
      if (sb.n_received != n_checked)
        `uvm_error("TLM_AUDIT", $sformatf("received %0d, checked %0d", sb.n_received, n_checked))
    endfunction

    Occupancy for anything with a FIFO

    A FIFO's used() tells you how far the consumer is behind. Recording the peak is one line in the consumer loop: if (fifo.used() > max_used) max_used = fifo.used(); just before each get. The peak is the most useful number you can have when sizing a bounded FIFO, and a peak that keeps rising across a long test is the signature of a consumer that cannot keep up.

    Watch it run: measured occupancy

    The slow FIFO scoreboard from imp_decl vs FIFOs records its peak. With an 80 ns compare and the DMA producing a word roughly every clock (10 ns) with occasional idle cycles, it reported:

    Diagram
    UVM_INFO tlm_tb.sv(334) @ 855000: uvm_test_top.env.fsb [FSB] out_fifo peaked at 5 waiting item(s)

    That number is worth comparing with the first-pass estimate. The excess of arrival rate over service rate across a burst is the standard starting point:

    Diagram
    excess = (arrival_rate - service_rate) * burst_window
    depth >= excess + margin
    
    8-word packet, about 1 word per 10 ns, 1 compare per 80 ns:
      all 8 words arrive within about 80-100 ns, while only 1 compare finishes
      estimate: about 6-7 waiting, measured peak: 5 (idle beats stretched the burst)

    The estimate was pessimistic because the input stream has random idle beats. Measure on your real traffic, take the worst peak across a regression, and add margin. For an analysis FIFO, which is unbounded, the same peak tells you how much memory a long test will use and how late a mismatch can be reported: an item behind five others waits about five service times before it is checked.

    Choose the loss policy deliberately

    Policy

    Behaviour

    Use it for

    Block when full

    Producer waits; nothing is lost

    Flow-controlled paths between components (put/get through uvm_tlm_fifo)

    Unbounded

    Producer never waits; memory grows

    Analysis paths: a monitor must never be slowed (analysis FIFO)

    Drop and count

    Producer continues; every drop is counted and reported

    Best-effort telemetry, never correctness checks

    A monitor-to-checker path must never block (the monitor would miss bus activity) and must never drop silently (the checker would pass on partial data). That leaves unbounded, with a check that it drained.

    What breaks: the consumer that died quietly

    A team's reset handler stopped and restarted the scoreboard's threads with disable fork when the DUT was reset mid-test. The disable fork was not isolated, so it also killed the scoreboard's out_fifo.get loop, which was a sibling in the same process tree. The monitors kept writing into the analysis FIFO; nobody read it. Because the FIFO was unbounded, nothing blocked and nothing errored. The scoreboard compared nothing after the reset, and its only check was "no mismatches", so the test passed. Long tests used gigabytes of memory, which was the only symptom anyone noticed.

    Two habits would each have caught it on the first run. A check_phase that fails when any analysis FIFO is not empty, as dma_fifo_scoreboard does with FSB_UNREAD. And received-versus-checked counters. The root cause, disable fork killing every child of the current process, has its own fix: wrap it in fork begin ... end join so it only reaches the threads you meant.

    Interview

    "The scoreboard reports fewer transactions than the monitor published. How do you debug it?"

    A weak answer: "I'd add prints in the monitor and scoreboard and look at the waveform." It may work, after a day.

    A strong answer: "Structure first, then counts. Errors at time 0 and the topology tell me whether the components and links exist. debug_connected_to on the monitor's port tells me whether the checker is really on its fan-out. Then counters: published versus received isolates the connection or a filter; received versus checked isolates a stalled thread or a backlog, which a FIFO's used() and an end-of-test emptiness check confirm. Analysis paths are unbounded by design, so a dead consumer shows up as memory growth, not as an error, unless check_phase checks that the FIFOs drained."

    Expect the follow-ups: How do you size a bounded FIFO? (measured peak occupancy plus margin, checked against burst math) and Why not just make it deeper? (deep FIFOs delay mismatch reports and hide a slow consumer).

    Check yourself published == received but received > checked, and the gap is always the last few items of the test. What is the likely cause?

    The checker's threads were still working, or items were still queued, when the run phase ended. The scoreboard must drain before the phase ends (phase_ready_to_end with an objection while busy), and check_phase should report unread items rather than letting them vanish.

    Check yourself A stress seed shows a bounded uvm_tlm_fifo between two components reaching its depth of 16. Is that a bug?

    Not by itself. Reaching full means the producer was throttled, which is what a bounded FIFO is for. It is a bug only if the producer must never stall (a monitor, for example) or if the stall breaks a protocol timing requirement. Then fix the consumer's rate or the architecture, not just the depth.

    Check yourself Why is an unbounded analysis FIFO the right default between a monitor and a checker, despite the memory risk?

    A monitor must never block, or it misses bus activity, and a checker must never silently lose items. Unbounded is the only policy that satisfies both. The memory risk is handled by checking at end of test that the FIFO drained and by watching its peak occupancy.

    Next: the TLM 1 families carry any transaction type with direct flow control. TLM 2.0 sockets standardize one memory-mapped transaction and add timing annotation, which is what you meet when a UVM bench talks to a SystemC model.

    Related topics

    • imp_decl vs FIFOs

    • TLM1 channels and port cardinality

    • Mismatch triage

    • TLM 2.0 sockets

    Previous

    66 / 280

    Next: The TLM Lessons' Bench File: Full Source

    Up next

    Up nextThe TLM Lessons' Bench File: Full Source

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Who Calls, Who Implements: Ports, Exports and Imps
    • connect() Rules: Which Way Each Link Points
    • put, get, peek and transport: Flow-Controlled TLM