Ports, exports and imps: who calls whom

Read a TLM connection as a chain of calls: tell the caller from the provider, say which way the data moves for put, get and transport, connect through two levels of hierarchy, and diagnose the errors UVM reports when a chain is wrong.

Lesson
U3.2
Time
35 min
Runs on
Verilator with UVM
Check
5 questions

A producer sits two levels inside one environment and its consumer two levels inside another. The test calls connect on the wrong end, and the run stops at time 0 with four connection errors and stopping due to build errors. To fix it you need to know which object calls, which one provides, and where each connect belongs.

Five connect calls, one put callArrows are connect calls, from the caller side towards the provider. Ports climb out of the sending environment, one export link crosses between environments, and exports descend to the imp.
port (calls out)export (forwards)imp (implements)connect() linkcall: who calls whom
Five connect calls, one put callArrows are `connect` calls, from the caller side towards the provider. Ports climb out of the sending environment, one export link crosses between environments, and exports descend to the imp. port src.put_port Port of src_env port src.agent.put_port Port of the agent port prod.put_port The producer calls puthere export snk.put_export Export of snk_env export snk.agent.put_export Export of the agent imp cons.put_imp Calls consumer.put(t) prod.put_port.connect(put_port)agent.put_port.connect(put_port)src.put_port.connect(snk.put_export)put_export.connect(agent.put_export)put_export.connect(cons.put_imp)at run time: put(t)call
  1. Up

    Each agent and environment connects its child's port to its own port: child.port.connect(parent_port). Two such calls lift the port to the edge of src.

    More context

    A connect arrow only records a link in connect_phase. Nothing ever travels along it, so these steps show no moving token.

  2. Across

    The test, parent of both environments, makes the one sibling link: port to export.

  3. Down

    Each receiving parent connects its own export to its child's export or imp: parent_export.connect(child.export).

  4. Resolve

    Just before end of elaboration, UVM walks every chain and gives each port the list of imps at its end. All four links report size() = 1.

    More context

    The first lines of the run show the result: every size() is 1, and the resolved list names cons.put_imp.

  5. Call10 ns (illustrative)

    At 10 ns the producer calls put_port.put(#1). The resolved port calls the imp directly, which calls consumer.put. The producer's thread waits 3 ns inside it.

    More context

    The call follows the resolved arrow, not the five connect links: the four ports and exports in between run no code.

    Follow the calls, values and waits
    1. call put(#1): prod.put_port → cons.put_imp
    2. Waiting: prod.put_port — producer inside consumer.put until 13 ns
  6. Return13 ns (illustrative)

    At 13 ns consumer.put stores #1 and returns; the producer prints put(#1) returned.

    More context

    Trace lines 3 and 4. put has no output argument, so only control comes back to the producer.

    Follow the calls, values and waits
    1. Released: prod.put_port
Read the diagram as text
  • src.put_port: Port of src_env
  • src.agent.put_port: Port of the agent
  • prod.put_port: The producer calls put here
  • snk.put_export: Export of snk_env
  • snk.agent.put_export: Export of the agent
  • cons.put_imp: Calls consumer.put(t)
  • prod.put_port → src.agent.put_port: prod.put_port.connect(put_port)
  • src.agent.put_port → src.put_port: agent.put_port.connect(put_port)
  • src.put_port → snk.put_export: src.put_port.connect(snk.put_export)
  • snk.put_export → snk.agent.put_export: put_export.connect(agent.put_export)
  • snk.agent.put_export → cons.put_imp: put_export.connect(cons.put_imp)
  • prod.put_port → cons.put_imp: at run time: put(t) call

The five connect lines build a netlist. At run time, the call skips the intermediate ports and exports.

Three roles: port, export, imp

Every TLM object has one of three roles. They are all classes derived from uvm_port_base, and they differ in which side of a call they sit on.

RoleSymbolSaysCreated withWho holds it
Portsquare"I will call these methods"new("put_port", this)The initiator, or a parent passing the need upward
Exportcircle"I provide these methods by forwarding"new("put_export", this)A parent exposing a child's imp
Impfilled circle"I provide these methods by calling my owner"new("put_imp", this) with type #(pkt, consumer)The component that implements put

Square, circle and filled circle are the usual drawing symbols for the three roles.

The imp is the end of every chain. Its type names the component, uvm_blocking_put_imp #(pkt, consumer), and its constructor takes the owner instead of a parent. When called, it calls put on that owner. U3.1 showed why it needs the exact class.

The component that calls is the initiator; the one that implements is the target. The initiator always owns a port, whichever way the data goes.

Control goes one way, data may go either way

The arrow in a TLM drawing shows who calls. Data follows the arguments: an input argument goes with the call, and an output argument comes back with the return.

InterfaceInitiator callsData movesTypical use
putput(t)Initiator to target, with the callA generator pushes packets to a driver model
getget(t)Target to initiator, on return; removes the itemA consumer pulls the next item when it is ready
peekpeek(t)Target to initiator; the item staysLook at the next item before deciding
transporttransport(req, rsp)Request in with the call, response out on returnOne request, one answer, in one call

All four are blocking: they are tasks, and the initiator waits until the target returns.

The example's auditor uses both reversed and two-way directions. It calls get_port.get(t) at 0 ns, before any packet exists, so its thread waits inside consumer.get. At 13 ns the consumer stores #1 and hands it out, and the auditor's get returns.

A get brings data back; a transport carries it both waysThe two arrows through agent.get_export are connect links, used only to resolve the chain. The run-time get goes straight from the auditor's port to the resolved imp; arrows labelled returns carry values back to the caller.
export (forwards)imp (implements)connect() linkcall: who calls whomdata: where the transaction goes
A get brings data back; a transport carries it both waysThe two arrows through `agent.get_export` are `connect` links, used only to resolve the chain. The run-time `get` goes straight from the auditor's port to the resolved imp; arrows labelled *returns* carry values back to the caller. component auditor get_port andtr_port: the initiatorof both calls export agent.get_export Wiring only: forwardsduring resolution imp cons.get_imp The resolved imp;calls consumer.get imp resp.tr_imp Callsresponder.transport connect:aud.get_port toagent.get_exportconnect: get_exportto cons.get_impget(t) call, resolvedt returnstransport call, reqgoes inrsp returns
  1. Wire

    In connect_phase two links pass the auditor's port through the agent's export to the consumer's imp. Resolution stores that imp in the port's m_if.

    More context

    Connect links are wiring only. No call or packet ever travels along them at run time.

  2. Ask at 0 ns

    The auditor calls get_port.get(t). The call goes directly to cons.get_imp and on to consumer.get; the export does not run. No data moves yet.

    More context

    Trace line 1. The consumer's store is empty, so the auditor's own thread now waits inside consumer.get.

    Follow the calls, values and waits
    1. call get(t): auditor → cons.get_imp
    2. Waiting: auditor — blocked inside consumer.get until 13 ns
  3. Data back at 13 ns

    The producer's put has just stored #1. get pops it into its output argument and returns: the data moves against the call.

    More context

    Trace lines 3 to 6 share 13 ns: the store fills, consumer.get hands #1 out, and the auditor's get, called at 0 ns, returns.

    Follow the calls, values and waits
    1. return #1: cons.get_imp → auditor
    2. Released: auditor
  4. Request in at 13 ns

    The auditor calls transport(#1, rsp). The request goes in with the call, and the responder works for 2 ns inside it.

    More context

    Trace line 7. The request is an input argument, so it moves with the call, from initiator to target.

    Follow the calls, values and waits
    1. data #1: auditor → resp.tr_imp
    2. Waiting: auditor — inside responder.transport until 15 ns
  5. Response out at 15 ns

    After 2 ns, #101 comes back in rsp at 15 ns, and transport returns.

    More context

    Trace lines 8 and 9. One call carried data both ways: the request in, the response back on return.

    Follow the calls, values and waits
    1. return #101: resp.tr_imp → auditor
    2. Released: auditor
Read the diagram as text
  • auditor: get_port and tr_port: the initiator of both calls
  • agent.get_export: Wiring only: forwards during resolution
  • cons.get_imp: The resolved imp; calls consumer.get
  • resp.tr_imp: Calls responder.transport
  • auditor → agent.get_export: connect: aud.get_port to agent.get_export
  • agent.get_export → cons.get_imp: connect: get_export to cons.get_imp
  • auditor → cons.get_imp: get(t) call, resolved
  • cons.get_imp → auditor: t returns
  • auditor → resp.tr_imp: transport call, req goes in
  • resp.tr_imp → auditor: rsp returns

Who calls never changes: the auditor owns both ports. Only the data direction differs between put, get and transport.

A blocking call runs the target's code in the initiator's thread. That is why the producer's trace shows put(#1) returned at 13 ns, three nanoseconds after it called. The target can make an initiator wait as long as it likes.

Connecting through the hierarchy

A connection is made by calling connect on the caller side with the provider as argument, always requirer.connect(provider). Which objects may meet depends on where they sit.

ConnectionFormWritten inIn the example
Port to portchild.port.connect(parent_port)The parentLifting put_port out of the agent and the env
Port to export or impa.port.connect(b.export)The common parent of siblingssrc.put_port.connect(snk.put_export)
Export to export or impparent_export.connect(child.export)The parentput_export.connect(cons.put_imp)
Export to portNot allowedThe fault below
Imp to anythingNot allowedAn imp is bound to its owner when constructed

Ports go up and across, exports go down. An imp never calls connect.

Each connect call only records a link, normally in connect_phase; one made at or after end of elaboration is ignored with a Late Connection warning. When end_of_elaboration_phase starts, UVM resolves the links: each port follows its chain and stores the imps it reaches. After that, size() returns that count. Two reports draw the chain. debug_connected_to() on a port prints its fanout down to the resolved imps. debug_provided_to() on an imp prints its fanin, back up to every port that reaches it.

One put call, hop by hop through the libraryArrows to the right are calls, each a task call in the producer's own thread; the arrow from the binding node is configuration, done once. The node text shows the method the library generates for each class from its macros.
port (calls out)imp (implements)call: who calls whom
One put call, hop by hop through the libraryArrows to the right are calls, each a task call in the producer's own thread; the arrow from the binding node is configuration, done once. The node text shows the method the library generates for each class from its macros. component producer.run_phase put_port.put(t) port uvm_blocking_put_port Generated: task put(T t);this.m_if.put(t); endtask binding resolution, before endof elaboration set_if(0): m_if =cons.put_imp, the firstresolved imp imp uvm_blocking_put_imp#(pkt, consumer) Generated: task put(T t);m_imp.put(t); endtask component consumer::put Your task: #3, store,trigger stored put(t) callsets m_ifm_if.put(t): virtualcallm_imp.put(t): directcall
  1. Bind once

    When end of elaboration starts, resolve_bindings walks the five links and set_if(0) stores the one imp it found in the port's m_if. The exports in between are never called again.

    More context

    This is an assignment made once, before time 0, not a call made during the run. It is what lets every later put skip the intermediate exports.

    Follow the calls, values and waits
    1. data m_if = cons.put_imp: resolution, before end of elaboration → uvm_blocking_put_port
  2. Hop 1: the port10 ns (illustrative)

    At 10 ns the producer calls put_port.put(#1). The port's put was written by the UVM_BLOCKING_PUT_IMP(this.m_if, T, t) macro: it just calls m_if.put(t).

    More context

    Trace line 2. Packet #1 rides as the call's argument; from here on the producer's thread is inside the call.

    Follow the calls, values and waits
    1. call put(#1): producer.run_phase → uvm_blocking_put_port
    2. Waiting: producer.run_phase — inside put(#1) until 13 ns
  3. Hop 2: the imp10 ns (illustrative)

    m_if is declared with the base type, so dispatch runs the imp's put, written by UVM_BLOCKING_PUT_IMP(m_imp, T, t): it calls m_imp.put(t).

    More context

    Still 10 ns: each hop is a plain task call, so no simulated time passes between them.

    Follow the calls, values and waits
    1. call put(#1): uvm_blocking_put_port → uvm_blocking_put_imp #(pkt, consumer)
  4. Hop 3: your code10 to 13 ns (illustrative)

    m_imp is a consumer handle, so consumer::put runs. It waits 3 ns and stores #1 in its own queue, still in the producer's thread.

    More context

    The #3 is the only delay in the chain, and the producer's thread is the one that waits it.

    Follow the calls, values and waits
    1. call put(#1): uvm_blocking_put_imp #(pkt, consumer) → consumer::put
    2. Waiting: producer.run_phase — inside consumer::put until 13 ns
    3. Released: producer.run_phase
  5. Return13 ns (illustrative)

    Each task returns to its caller, and the producer prints put(#1) returned at 13 ns. The port and the imp added no queue and no process; the only storage is the consumer's own store.

    More context

    Trace lines 3 and 4. put has no output argument, so nothing travels back along the calls: the three tasks simply return in turn and the producer resumes.

    Follow the calls, values and waits
    1. Released: producer.run_phase
Read the diagram as text
  • producer.run_phase: put_port.put(t)
  • uvm_blocking_put_port: Generated: task put(T t); this.m_if.put(t); endtask
  • resolution, before end of elaboration: set_if(0): m_if = cons.put_imp, the first resolved imp
  • uvm_blocking_put_imp #(pkt, consumer): Generated: task put(T t); m_imp.put(t); endtask
  • consumer::put: Your task: #3, store, trigger stored
  • producer.run_phase → uvm_blocking_put_port: put(t) call
  • resolution, before end of elaboration → uvm_blocking_put_port: sets m_if
  • uvm_blocking_put_port → uvm_blocking_put_imp #(pkt, consumer): m_if.put(t): virtual call
  • uvm_blocking_put_imp #(pkt, consumer) → consumer::put: m_imp.put(t): direct call

Two library methods of one line each stand between your producer and your consumer.

That is all the communication there is: ordinary task calls, chosen at end of elaboration. The intermediate ports and exports exist so that each component can be wired by its own parent without knowing what lies outside. Which methods each class may generate is set by its interface, the subject of U3.3.

When the wiring is wrong

Each port has a minimum and a maximum number of imps it must reach, checked at resolution. They are constructor arguments, and for put, get and transport ports both default to 1. A port built with a larger maximum may bind several imps, but each call still goes to the one selected by set_if, index 0 by default. Only an analysis port calls every imp it reaches, and it may reach none. Any error before end of elaboration stops the run.

MistakeWhat this library reportsSeverity
No connection for a portconnection count of 0 does not meet required minimum of 1, once per port in the chainUVM_ERROR, then BUILDERR fatal
snk.put_export.connect(src.put_port)Cannot connect exports to ports Try calling port.connect(export) instead.UVM_ERROR, then fatal
Get port to a transport imp (U3.3 explains the check)... does not provide the complete interface required of this portUVM_ERROR, then fatal
One put port with the default maximum reaching two impsconnection count of 2 exceeds maximum of 1UVM_ERROR, then fatal
Skipping a level: test connects prod.put_port to cons.put_impNothing by default; the call worksWarning only with check_connection_relationships

Measured in this course's bench. The run never starts after a build error.

The last row is the quiet one. Connecting across several levels works, but it ties the test to the inner names of both environments. Setting check_connection_relationships to 1 in the configuration database before the ports are built turns it into a Connection Warning.

Try it

The size() line and the two reports are printed at end of elaboration, after resolution. debug_connected_to draws the chain from the producer's port down and lists one resolved imp; debug_provided_to draws the same chain from the imp back up to the producer's port.

In the trace, every put takes 3 ns of the producer's time, and every get returns in the same step as the put that feeds it. Try it: swap the test's connect to snk.put_export.connect(src.put_port); and read the four errors from the top.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: size(): prod.put_port=1 agent.put_port=1 src.put_port=1 snk.put_export=1 Resolved implementation list: 0: uvm_test_top.snk.agent.cons.put_imp (uvm_blocking_put_imp) uvm_test_top.snk.agent.cons.put_imp (uvm_blocking_put_imp) |_uvm_test_top.src.agent.prod.put_port (uvm_blocking_put_port) 1 0 ns auditor: get_port.get(t) 2 10 ns producer: put_port.put(#1) 3 13 ns consumer.put: stored #1 after 3 ns 4 13 ns producer: put(#1) returned 5 13 ns consumer.get: hands #1 to the caller 6 13 ns auditor: get returned #1 7 13 ns auditor: tr_port.transport(#1, rsp) 8 15 ns responder.transport: request #1, response #101 9 15 ns auditor: transport returned #101 10 15 ns auditor: get_port.get(t) 11 23 ns producer: put_port.put(#2) 12 26 ns consumer.put: stored #2 after 3 ns 13 26 ns producer: put(#2) returned 14 26 ns consumer.get: hands #2 to the caller 15 26 ns auditor: get returned #2 16 26 ns auditor: tr_port.transport(#2, rsp) 17 28 ns responder.transport: request #2, response #102 18 28 ns auditor: transport returned #102 UVM_ERROR : 0

Check yourself

Predict the outputThe test calls gp.get(t) on a source whose get takes 4 ns and numbers packets from 7, then tp.transport(t, r) on a responder that takes 2 ns and adds 100. What line does it print?
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;

class pkt extends uvm_sequence_item;
  `uvm_object_utils(pkt)
  int id;
  function new(string name = "pkt"); super.new(name); endfunction
endclass

class source extends uvm_component;
  `uvm_component_utils(source)
  uvm_blocking_get_imp #(pkt, source) get_imp;
  int next_id = 7;
  function new(string name, uvm_component parent); super.new(name, parent); get_imp = new("get_imp", this); endfunction
  task get(output pkt t);
    #4;                                     // making a packet takes 4 ns
    t = pkt::type_id::create("t");
    t.id = next_id++;
  endtask
endclass

class responder extends uvm_component;
  `uvm_component_utils(responder)
  uvm_blocking_transport_imp #(pkt, pkt, responder) tr_imp;
  function new(string name, uvm_component parent); super.new(name, parent); tr_imp = new("tr_imp", this); endfunction
  task transport(pkt req, output pkt rsp);
    #2;
    rsp = pkt::type_id::create("rsp");
    rsp.id = req.id + 100;
  endtask
endclass

class pull_test extends uvm_test;
  `uvm_component_utils(pull_test)
  source    src;
  responder rs;
  uvm_blocking_get_port #(pkt)            gp;
  uvm_blocking_transport_port #(pkt, pkt) tp;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    src = source::type_id::create("src", this);
    rs  = responder::type_id::create("rs", this);
    gp  = new("gp", this);
    tp  = new("tp", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    gp.connect(src.get_imp);
    tp.connect(rs.tr_imp);
  endfunction
  task run_phase(uvm_phase phase);
    pkt t, r;
    phase.raise_objection(this);
    gp.get(t);
    tp.transport(t, r);
    $display("%0d ns: got #%0d, response #%0d", $time, t.id, r.id);
    phase.drop_objection(this);
  endtask
endclass

module tb;
  initial run_test("pull_test");
endmodule
Spot the bugEach option is the test's connect_phase for the two-environment bench. The run passes when the auditor receives both packets with no error. Which option fails?
ConceptA checker calls get_port.get(t) on a port connected to a sequence-history component's get imp. Which description of the call is right?
ConceptAn agent contains a monitor-like producer whose put_port must reach a component outside the agent. Which line belongs in the agent's connect_phase?
ConceptA run stops at time 0 with connection count of 0 does not meet required minimum of 1 on src.put_port, src.agent.put_port and src.agent.prod.put_port, then stopping due to build errors. What is the most likely single cause?

Remember

  • A port is the calling side, an export provides by forwarding, and an imp provides by calling its owner; the imp's type names the owner's class.
  • The initiator always calls. Put sends data with the call, get and peek bring it back on return, transport does both.
  • Ports connect up (child.port.connect(parent_port)) and across (port.connect(export)); exports connect down (export.connect(child.export)); an imp never calls connect.
  • UVM resolves chains just before end of elaboration: with default bounds a put, get or transport port must reach exactly one imp, and any error stops the run before time advances.

Practice