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. Who Calls, Who Implements: Ports, Exports and Imps

Part 4/TLM & AnalysisIntermediate

Who Calls, Who Implements: Ports, Exports and Imps

Port, export and imp defined by who makes the call and who runs the code, shown on the DMA bench's monitor and driver links, with push versus pull and the subscriber that owned a port and heard nothing.

On this page

    You review a colleague's new checker. It declares uvm_analysis_port #(apb_item) in_port, defines a write() method, and the env connects a monitor's port to it. It compiles. Elaboration is silent. The test passes with zero errors. Then you read the checker's own report line: 0 item(s) received. Nothing in the library complained, because nothing was illegal. The checker simply owned the wrong kind of object. Which side of a TLM link owns which object, and why did no tool notice?

    The idea: the caller owns the port, the implementer owns the imp

    TLM (transaction-level modeling) is how UVM components exchange whole transaction objects: one component calls a method such as write(), put() or get(), and another component runs it. They never hold handles to each other. Instead, each end of a link is a small connection object, and connect() joins them before the run starts. That indirection is what lets you swap a scoreboard, add a coverage collector or move an agent to another env without editing the components themselves.

    Think of a phone call. The port is the handset of the person who dials: it says I need someone to call. The imp (implementation) is the phone that rings, wired to the person who answers and actually does the work. An export is a company switchboard: it has no one behind it, it only forwards the call one level down to the right desk. Who dials says nothing about which way information flows. You can call to tell someone something (push: put, write) or call to ask for something (pull: get, peek). Either way, the caller owns the port and the answerer owns the imp.

    Object

    Owned by

    What it does

    Connects to

    Port

    The component that makes the call

    Declares which interface it needs and forwards each call along its connection

    An export or imp, or its parent's port

    Export

    A parent that hides a child

    Forwards a call one level down; runs nothing itself

    A child's export or imp

    Imp

    The component that defines the method

    Calls that component's method; ends the chain

    Nothing: an imp's own connect() is an error

    The picture: two links in the DMA bench

    Two links: the input monitor's analysis port calls write on the scoreboard's imp; the driver's seq_item_port calls get_next_item on the sequencer's seq_item_export, and the item returns to the driver.
    The monitor pushes and the driver pulls, but in both links the caller owns the port. On the pull link the data travels opposite to the call.

    The driver is the case that trips people up. It receives items, yet it owns the port, because it is the one that calls get_next_item. The item comes back as the call's output argument. Role follows the call, never the data.

    Build it: the three ends you meet every day

    The monitor end: a port that publishes

    The APB monitor creates its analysis port in the constructor and calls ap.write(it) once per completed transfer, with a freshly created item each time. The monitor does not know or care who is listening. That is the whole point: the same monitor works with one scoreboard, three subscribers, or none.

    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
    // snippet: from the course DMA bench
    class apb_monitor extends uvm_monitor;
      `uvm_component_utils(apb_monitor)
      virtual apb_if vif;
      uvm_analysis_port #(apb_item) ap;
    
      function new(string name, uvm_component parent);
        super.new(name, parent);
        ap = new("ap", this);
      endfunction
    
      function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        if (!uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif))
          `uvm_fatal("NOVIF", "apb_if not set for this monitor")
      endfunction
    
      // A transfer completes on the edge where PSEL, PENABLE and PREADY are all 1.
      task run_phase(uvm_phase phase);
        forever begin
          @(vif.mon_cb);
          if (vif.mon_cb.psel && vif.mon_cb.penable && vif.mon_cb.pready) begin
            apb_item it = apb_item::type_id::create("it");   // a fresh object per transfer
            it.addr   = vif.mon_cb.paddr;
            it.write  = vif.mon_cb.pwrite;
            it.data   = vif.mon_cb.pwdata;
            it.rdata  = vif.mon_cb.prdata;
            it.slverr = vif.mon_cb.pslverr;
            `uvm_info("APB_MON", it.convert2string(), UVM_HIGH)
            ap.write(it);
          end
        end
      endtask
    endclass

    The scoreboard end: an imp that names its implementer

    An imp takes two type parameters: the transaction type and the class that implements the method. The second one matters. Here is the library's analysis imp, trimmed to the lines that do the work:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // snippet: uvm_analysis_imp, trimmed from the UVM library source
    class uvm_analysis_imp #(type T = int, type IMP = int)
      extends uvm_port_base #(uvm_tlm_if_base #(T, T));
      local IMP m_imp;                 // the component that implements write()
    
      function new(string name, IMP imp);
        super.new(name, imp, UVM_IMPLEMENTATION, 1, 1);
        m_imp = imp;
      endfunction
    
      function void write(input T t);
        m_imp.write(t);                // the call lands in the component
      endfunction
    endclass

    You construct an imp with this as its second argument, so the imp is both a child of your component and a pointer back to it. When a port calls the imp's write(), the imp calls m_imp.write() on your component. Two consequences follow. Your class must define the method the imp calls, with the exact signature. And the call goes through a handle typed as the class you named in the parameter, which is why war story 2 happened.

    The driver end: a pull port and an imp called export

    The driver's seq_item_port is a uvm_seq_item_pull_port, declared for you in uvm_driver. The sequencer's seq_item_export is, despite its name, a uvm_seq_item_pull_imp: the sequencer itself implements get_next_item, item_done, get and peek. The agent joins them with one line in its connect_phase: drv.seq_item_port.connect(sqr.seq_item_export). The handshake on that link is the subject of the driver handshake lesson.

    Push and pull with put and get

    The same role rule holds for the flow-controlled families. A component that hands DMA jobs to a job runner owns a put port; the runner owns a put imp and implements put(). If instead the runner asks for the next job when it is ready, the runner owns a get port and the job source implements get().

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // snippet
    class job_source extends uvm_component;           // push side: calls put()
      `uvm_component_utils(job_source)
      uvm_blocking_put_port #(dma_job_cfg) job_port;
      function new(string name, uvm_component parent);
        super.new(name, parent);
        job_port = new("job_port", this);
      endfunction
    endclass
    
    class job_runner extends uvm_component;           // implements put()
      `uvm_component_utils(job_runner)
      uvm_blocking_put_imp #(dma_job_cfg, job_runner) job_imp;
      function new(string name, uvm_component parent);
        super.new(name, parent);
        job_imp = new("job_imp", this);
      endfunction
      task put(dma_job_cfg job);                      // blocks until the job is done
        `uvm_info("JOB", job.convert2string(), UVM_LOW)
      endtask
    endclass
    // in the parent's connect_phase: src.job_port.connect(runner.job_imp);

    Which family to use, and what blocking means for each call, is the subject of put, get, peek and transport.

    Watch it run: what the topology says

    The topology table printed by dma_base_test at end of elaboration lists every port, export and imp with its real class. These rows come from a dma_smoke_test run:

    Diagram
    apb                    apb_agent               -     @257
            drv                  apb_driver              -     @459
              rsp_port           uvm_analysis_port       -     @476
              seq_item_port      uvm_seq_item_pull_port  -     @467
            mon                  apb_monitor             -     @319
              ap                 uvm_analysis_port       -     @327
            sqr                  uvm_sequencer           -     @336
              rsp_export         uvm_analysis_export     -     @344
              seq_item_export    uvm_seq_item_pull_imp   -     @450
          ...
          sb                     dma_scoreboard          -     @281
            apb_export           uvm_analysis_imp_apb    -     @289
            in_export            uvm_analysis_imp_in     -     @299
            out_export           uvm_analysis_imp_out    -     @308
    • seq_item_export prints as uvm_seq_item_pull_imp: the name says export, the class says imp. Trust the class column.

    • The scoreboard's three inputs are imps of three different classes, one per ` uvm_analysis_imp_decl `` suffix.

    • The agent declares uvm_analysis_port #(apb_item) ap, yet no ap row appears under apb. The next lesson explains why.

    What breaks: the subscriber that owned a port

    Back to the opening. The checker was written like this:

    design.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // bad-example: a "subscriber" that owns a port instead of an imp
    class deaf_sb extends uvm_component;
      `uvm_component_utils(deaf_sb)
      uvm_analysis_port #(apb_item) in_port;     // should be an imp
      int unsigned n_seen;
      function new(string name, uvm_component parent);
        super.new(name, parent);
        in_port = new("in_port", this);
      endfunction
      function void write(apb_item t);           // never called
        n_seen++;
      endfunction
    endclass
    // parent connect_phase: tk.ap.connect(sb.in_port);   // compiles, elaborates, delivers nothing

    A small test, tlm_deaf_test, wrote three items into the publisher's port. The real log (the test is in tlm_tb.sv, the TLM lessons' file in the course bench):

    Diagram
    UVM_INFO tlm_tb.sv(30) @ 30000: uvm_test_top.tk [TALK] wrote 3 items
    UVM_INFO tlm_tb.sv(48) @ 30000: uvm_test_top.sb [DEAF] 0 item(s) received
    UVM_ERROR :    0
    UVM_FATAL :    0

    Every step was legal. A port may connect to another port (that is how a port is passed up through a parent). An analysis port skips the hierarchy check on its connections. And an analysis port with no imp at the end of its chain is allowed, because a monitor with no listeners is a normal thing. So write() walked a list of zero imps and returned. The checker's write() was just an ordinary method that nobody called. The fix is one line: declare uvm_analysis_imp #(apb_item, deaf_sb) in_imp and connect the publisher to it. The lesson behind it is the one the bench's scoreboard already applies: a checker must prove it checked something, which is why dma_scoreboard fails the test when it compared zero words.

    "What is the difference between a port, an export and an imp?"

    A weak answer ties the objects to data direction: "a port sends, an export receives, an imp is the end." The driver, which receives items through a port, breaks that rule at once.

    A strong answer ties them to the call: "A port belongs to the component that makes the call. An imp belongs to the component that implements the method, and its second type parameter names that class, so the imp can call back into it. An export only forwards a call one level down through a parent. Data can flow either way: the driver owns a pull port and gets the item as an output argument. The sequencer's seq_item_export is really an imp."

    Expect the follow-ups: Why can an imp not be connected to anything? (it ends the chain; calling its connect() is an error) and How does one component implement two `write()` methods? (with ` uvm_analysis_imp_decl ``, in Analysis broadcast).

    Check yourself A coverage collector must receive every APB item from the monitor. It extends uvm_component. What does it declare, and which method must it define?

    It declares uvm_analysis_imp #(apb_item, apb_cov) analysis_imp, constructs it with this, and defines function void write(apb_item t). The monitor's port is the caller; the collector implements. Extending uvm_subscriber #(apb_item) gives you that imp ready-made, as shown in imp_decl vs FIFOs.

    Check yourself Why does the driver, which receives every item, own the port on the sequencer link?

    Because it makes the call. The driver decides when it is ready and calls get_next_item; the sequencer implements it and returns the item through the output argument. Ownership follows the call, and the data simply travels back along it.

    Check yourself A scoreboard needs inputs from the APB monitor and from the stream monitor. Why can it not just declare two uvm_analysis_imp members?

    Both imps would call the same method name, write(), so the class could not tell the two streams apart (and both would need the same transaction type). ` uvm_analysis_imp_decl(_apb) ` generates a new imp class whose write() calls write_apb()`, which is how the bench's scoreboard gets three distinct methods.

    Next: the scoreboard sits in the env, two levels above the monitor's port. In connect() rules and pass-through you will see which way each link must point to cross that hierarchy, and why the agent's ap never appears in the topology.

    Related topics

    • connect() rules and pass-through

    • Analysis broadcast

    • Your first UVM testbench

    • The driver handshake

    Previous

    60 / 280

    Next: connect() Rules: Which Way Each Link Points

    Up next

    Up nextconnect() Rules: Which Way Each Link Points

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • put, get, peek and transport: Flow-Controlled TLM
    • Analysis Broadcast: One write(), Many Listeners
    • TLM1 Channels and Port Cardinality