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. Analysis Connectivity Patterns: imp_decl vs FIFOs

Part 4 · TLM & Analysis · Intermediate

Analysis Connectivity Patterns: imp_decl vs FIFOs

The DMA scoreboard rebuilt on uvm_tlm_analysis_fifo pairs with run_phase get() loops, run beside the imp_decl original: timing, blocking, ordering across streams, end-of-test draining, uvm_subscriber, and debug_connected_to / debug_provided_to.

On this page

    In code review, a senior engineer asks you to rewrite the DMA scoreboard with analysis FIFOs: cleaner, and the threads can wait for things. You do it, run both side by side, and both report 8 words matched. Then you give the new one a realistic, slower compare, and it fails a test the old one still passes:

    Diagram
    UVM_ERROR tlm_tb.sv(325) @ 655000: uvm_test_top.env.fsb [FSB_UNREAD] items left unread: apb=0 in=0 out=2
    UVM_ERROR tlm_tb.sv(327) @ 655000: uvm_test_top.env.fsb [FSB_LEFTOVER] 3 expected word(s) never came out
    UVM_INFO tlm_tb.sv(332) @ 655000: uvm_test_top.env.fsb [FSB] 5 words matched, 0 mismatched
    UVM_INFO tlm_tb.sv(334) @ 655000: uvm_test_top.env.fsb [FSB] out_fifo peaked at 5 waiting item(s)
    UVM_INFO dma_tb.sv(411) @ 655000: uvm_test_top.env.sb [SB] 8 words matched, 0 mismatched

    Same traffic, same DUT, same moment. What exactly do you trade when you move a scoreboard from imp_decl callbacks to FIFOs?

    The idea: a judge at the pass, or a rack the judge works through

    With `imp_decl`, the monitor's write() calls straight into the scoreboard's write_out(). The check runs in the monitor's thread, at the moment of sampling, in zero time. It cannot wait for anything, and when the monitor returns, the check is done. With analysis FIFOs, the monitor's write() drops the item into a uvm_tlm_analysis_fifo and returns; the scoreboard's own threads get() items whenever they are ready. Those threads may wait, call a slow reference model, or synchronize with a DUT event.

    It is the difference between a food critic standing at the kitchen pass, judging each plate as it leaves, and plates going onto a rack that the critic works through at their own pace. The rack lets the critic take as long as needed per plate. It also means the kitchen can close with plates still on the rack.

    The picture

    The output monitor's port feeds the imp_decl scoreboard, which compares inside write_out, and an analysis FIFO, from which the FIFO scoreboard's run_phase thread gets items and compares.
    One monitor write reaches both scoreboards. The imp_decl path finishes the check before write() returns; the FIFO path only queues the item, and the check happens later in the scoreboard's own thread.

    Build it: the same checks on three analysis FIFOs

    The FIFO scoreboard

    The predictor and comparator are the same as dma_scoreboard's. What changes is where they run: three forever loops in run_phase, one per FIFO. check_phase gains a check the imp_decl version never needed, that every FIFO was read to the end.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    // snippet: from tlm_tb.sv in the course bench
    class dma_fifo_scoreboard extends uvm_scoreboard;
      `uvm_component_utils(dma_fifo_scoreboard)
      uvm_tlm_analysis_fifo #(apb_item)  apb_fifo;
      uvm_tlm_analysis_fifo #(axis_item) in_fifo, out_fifo;
      bit [31:0]   key;
      bit [31:0]   expected[$];
      int unsigned n_match, n_mismatch;
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
        apb_fifo = new("apb_fifo", this);
        in_fifo  = new("in_fifo", this);
        out_fifo = new("out_fifo", this);
      endfunction
    
      task run_phase(uvm_phase phase);
        fork
          forever begin apb_item t;  apb_fifo.get(t); if (t.write && !t.slverr && t.addr == KEY) key = t.data; end
          forever begin axis_item t; in_fifo.get(t);  expected.push_back(t.data ^ key); end
          forever begin axis_item t; out_fifo.get(t); compare(t); end
        join
      endtask
    
      virtual function void compare(axis_item t);
        // same as dma_scoreboard::write_out: pop expected, compare, count
      endfunction
    
      function void check_phase(uvm_phase phase);
        if (!apb_fifo.is_empty() || !in_fifo.is_empty() || !out_fifo.is_empty())
          `uvm_error("FSB_UNREAD", $sformatf("items left unread: apb=%0d in=%0d out=%0d",
                     apb_fifo.used(), in_fifo.used(), out_fifo.used()))
        if (expected.size() != 0)
          `uvm_error("FSB_LEFTOVER", $sformatf("%0d expected word(s) never came out", expected.size()))
        if (n_match == 0) `uvm_error("FSB_EMPTY", "no words were compared")
      endfunction
    endclass

    Wiring it beside the original

    An env subclass builds the second scoreboard and adds a second subscriber to every monitor port. A test swaps the env in with a factory override and prints both ends of one link at end of elaboration:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    // snippet: from tlm_tb.sv in the course bench
    class dma_fifo_env extends dma_env;
      `uvm_component_utils(dma_fifo_env)
      dma_fifo_scoreboard fsb;
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
      function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        fsb = dma_fifo_scoreboard::type_id::create("fsb", this);
      endfunction
      function void connect_phase(uvm_phase phase);
        super.connect_phase(phase);                        // ap -> sb imps, as before
        apb.ap.connect(fsb.apb_fifo.analysis_export);      // a second subscriber per port
        in_agt.ap.connect(fsb.in_fifo.analysis_export);
        out_agt.ap.connect(fsb.out_fifo.analysis_export);
      endfunction
    endclass
    
    class dma_fifo_sb_test extends dma_smoke_test;
      `uvm_component_utils(dma_fifo_sb_test)
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
      function void build_phase(uvm_phase phase);
        dma_env::type_id::set_type_override(dma_fifo_env::get_type());
        super.build_phase(phase);
      endfunction
      function void end_of_elaboration_phase(uvm_phase phase);
        dma_fifo_env fenv;
        $cast(fenv, env);
        env.in_agt.ap.debug_connected_to();                  // fan-out of the publisher
        fenv.fsb.in_fifo.analysis_export.debug_provided_to(); // fan-in of one subscriber
      endfunction
    endclass

    debug_connected_to() prints a port's fan-out network and the resolved list of imps it calls; debug_provided_to() prints what feeds a connector. Call them at or after end of elaboration, when the bindings are resolved. They are part of the Accellera library (1.2 and the 1800.2 reference implementation) rather than the IEEE 1800.2 standard text.

    uvm_subscriber: the one-stream shortcut

    For a component with a single input, such as a coverage collector, uvm_subscriber #(T) declares the imp for you. Its field is analysis_export (an imp despite the name, whose instance name is analysis_imp), and write() is pure virtual, so the compiler makes you implement it. It cannot take a second stream; for that you are back to imp_decl or FIFOs.

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // snippet
    class apb_cov extends uvm_subscriber #(apb_item);
      `uvm_component_utils(apb_cov)
      int unsigned n_key_writes;
      function new(string name, uvm_component parent);
        super.new(name, parent);
      endfunction
      function void write(apb_item t);
        if (t.write && t.addr == KEY) n_key_writes++;   // sample a covergroup here
      endfunction
    endclass
    // env connect_phase: apb.ap.connect(cov.analysis_export);

    Watch it run

    dma_fifo_sb_test on the bench plus these additions. The debug calls print the publisher's fan-out, in the order write() will call it, and the FIFO's single source:

    Diagram
    UVM_INFO @ 0: uvm_test_top.env.in_agt.mon.ap [debug_connected_to] This port's fanout network:
    
      uvm_test_top.env.in_agt.mon.ap (uvm_analysis_port)
        |
        |_uvm_test_top.env.fsb.in_fifo.analysis_export (uvm_analysis_imp)
        |
        |_uvm_test_top.env.sb.in_export (uvm_analysis_imp_in)
    
      Resolved implementation list:
      0: uvm_test_top.env.fsb.in_fifo.analysis_export (uvm_analysis_imp)
      1: uvm_test_top.env.sb.in_export (uvm_analysis_imp_in)
    
    UVM_INFO @ 0: uvm_test_top.env.fsb.in_fifo.analysis_export [debug_provided_to] This port's fanin network:
    
      uvm_test_top.env.fsb.in_fifo.analysis_export (uvm_analysis_imp)
        |
        |_uvm_test_top.env.in_agt.mon.ap (uvm_analysis_port)
    
    UVM_INFO tlm_tb.sv(332) @ 655000: uvm_test_top.env.fsb [FSB] 8 words matched, 0 mismatched
    UVM_INFO dma_tb.sv(411) @ 655000: uvm_test_top.env.sb [SB] 8 words matched, 0 mismatched
    • The port is named in_agt.mon.ap: the agent's ap is the monitor's object, as in connect() rules.

    • fsb is first in the resolved list although it was connected second: the alphabetical order from Analysis broadcast.

    • Both scoreboards matched all 8 words. With an instant compare the two designs are indistinguishable.

    What breaks: the kitchen closes with plates on the rack

    Now make each output compare take 80 ns (dma_fifo_slow_test), as it would if the FIFO scoreboard called a slow reference model (the imp_decl scoreboard could not do this at all: a function cannot wait). The smoke test drops its objection 100 ns after its final STATUS read, which is plenty for the DUT and nowhere near enough for the slow consumer. The run phase ends, the scoreboard's get loops are killed with it, and check_phase finds the backlog: two words still in out_fifo, one more taken out but never finished, so three predictions never matched. The imp_decl scoreboard, which compares inside write(), has nothing pending and matches all 8.

    The fix is the same idea as the bench's dma_scoreboard_eot: a scoreboard with work in flight holds the run phase open itself. Here the slow variant (dma_fifo_drain_test, scoreboard dma_fifo_scoreboard_eot) raises an objection in phase_ready_to_end while any FIFO is non-empty or a compare is running (idle() checks the three FIFOs and a busy flag):

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // snippet: from tlm_tb.sv in the course bench
    function void phase_ready_to_end(uvm_phase phase);
      if (phase.get_imp() != uvm_run_phase::get() || idle()) return;
      phase.raise_objection(this, "analysis fifos not drained");
      fork begin
        while (!idle()) #10ns;
        phase.drop_objection(this, "analysis fifos drained");
      end join_none
    endfunction
    Diagram
    UVM_INFO tlm_tb.sv(332) @ 855000: uvm_test_top.env.fsb [FSB] 8 words matched, 0 mismatched
    UVM_INFO tlm_tb.sv(334) @ 855000: uvm_test_top.env.fsb [FSB] out_fifo peaked at 5 waiting item(s)
    UVM_INFO dma_tb.sv(411) @ 855000: uvm_test_top.env.sb [SB] 8 words matched, 0 mismatched
    UVM_ERROR :    0

    The run now ends at 855 ns instead of 655 ns, when the last compare finishes. Draining FIFOs covers only what the monitors have already published; for words the DUT still owes, the stronger rule from war story 3 applies: decide "done" from what the DUT was asked to do.

    The trade-offs in one table

    imp_decl callbacks

    Analysis FIFOs + get() loops

    When the check runs

    Inside the monitor's write(), at sample time

    Later, in the scoreboard's thread (same timestep if it is idle)

    Can the check wait or consume time?

    No: write_* are functions

    Yes: call models, wait for events, delay

    Order across streams

    Follows the order the monitors call write()

    Lost between separate FIFOs and threads; merge it yourself if it matters

    End of test

    Nothing pending when the monitor returns

    Must drain: check is_empty() and hold the phase while work is queued

    Memory

    Only what you store

    Unbounded FIFOs grow if a thread stalls or dies

    Extending by override

    Make write_* virtual (war story 2)

    Override virtual helpers such as compare()

    Debug

    Counters only

    used() shows backlog; peak occupancy is a sizing input

    A practical rule: use imp_decl when every check can be finished in zero time from what has been observed so far, which covers most in-order scoreboards. Use FIFOs when the checker must wait, for a reference model, a DUT side-band, or a matching transaction on a slower interface, and then own the draining at end of test. Mixing is fine: a scoreboard can take register writes through an imp_decl callback, so the key is always current, and data through a FIFO.

    Interview

    "Would you build a scoreboard with uvm_analysis_imp_decl or with analysis FIFOs?"

    A weak answer picks one as "best practice" without a reason, or says FIFOs are needed "to avoid losing transactions" (an imp never loses one).

    A strong answer: "It depends on whether the check can finish inside write(). imp_decl callbacks run in the monitor's thread in zero time, so nothing is pending at end of test, but they cannot wait. Analysis FIFOs let the scoreboard's own threads pull, block and call slow models, at the cost of losing cross-stream order between FIFOs and having to drain them: check is_empty in check_phase and hold the run phase while work is queued. I often mix them: configuration through a callback, data through a FIFO."

    Expect the follow-ups: How do you see which subscribers a port reaches? (debug_connected_to() at end of elaboration) and What does uvm_subscriber give you? (one ready-made imp and a pure virtual write).

    Check yourself A predictor must wait three clocks after each input word for a DUT side-band before predicting. Which design fits, and why?

    The FIFO design, or at least a FIFO for that stream. The wait needs a task running in its own thread; a write_in callback is a function in the monitor's thread and cannot wait. The scoreboard must then drain that FIFO and hold the phase at end of test.

    Check yourself In a different DUT, a KEY write and the first input word can be sampled on the same clock edge. The scoreboard uses two separate analysis FIFOs, one per stream. What can go wrong, and how do you fix it?

    The two get loops are separate threads, so the input word may be processed before the KEY write and predicted with the old key. Fixes: take the register stream through an imp_decl callback (processed in the monitor's call order, before the data thread runs), or merge both streams into one ordered queue, or timestamp items and order by time.

    Check yourself Why can a coverage collector extend uvm_subscriber, while the DMA scoreboard cannot?

    uvm_subscriber provides exactly one analysis imp and one write(). A coverage collector usually listens to one monitor. The scoreboard needs three streams with three different methods, which takes imp_decl (or three FIFOs).

    Next: when a transaction goes missing, you now know every place it can hide. Connection triage and FIFO sizing turns that into a playbook, and uses the peak occupancy above to size a FIFO.

    Related topics

    • Analysis broadcast

    • TLM1 channels and port cardinality

    • End-of-test strategy

    • Coverage subscribers

    Previous

    65 / 280

    Next: Connection Triage and FIFO Sizing: A Debug Playbook

    Up next

    Up nextConnection Triage and FIFO Sizing: A Debug Playbook

    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