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.
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.- 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 ofsrc.More context
A
connectarrow only records a link inconnect_phase. Nothing ever travels along it, so these steps show no moving token. - Across
The test, parent of both environments, makes the one sibling link: port to export.
- Down
Each receiving parent connects its own export to its child's export or imp:
parent_export.connect(child.export). - 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 namescons.put_imp. - Call10 ns (illustrative)
At 10 ns the producer calls
put_port.put(#1). The resolved port calls the imp directly, which callsconsumer.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
- call put(#1): prod.put_port → cons.put_imp
- Waiting: prod.put_port — producer inside consumer.put until 13 ns
- Return13 ns (illustrative)
At 13 ns
consumer.putstores #1 and returns; the producer printsput(#1) returned.More context
Trace lines 3 and 4.
puthas no output argument, so only control comes back to the producer.Follow the calls, values and waits
- 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.
| Role | Symbol | Says | Created with | Who holds it |
|---|---|---|---|---|
| Port | square | "I will call these methods" | new("put_port", this) | The initiator, or a parent passing the need upward |
| Export | circle | "I provide these methods by forwarding" | new("put_export", this) | A parent exposing a child's imp |
| Imp | filled 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.
| Interface | Initiator calls | Data moves | Typical use |
|---|---|---|---|
| put | put(t) | Initiator to target, with the call | A generator pushes packets to a driver model |
| get | get(t) | Target to initiator, on return; removes the item | A consumer pulls the next item when it is ready |
| peek | peek(t) | Target to initiator; the item stays | Look at the next item before deciding |
| transport | transport(req, rsp) | Request in with the call, response out on return | One 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.
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.- Wire
In
connect_phasetwo links pass the auditor's port through the agent's export to the consumer's imp. Resolution stores that imp in the port'sm_if.More context
Connect links are wiring only. No call or packet ever travels along them at run time.
- Ask at 0 ns
The auditor calls
get_port.get(t). The call goes directly tocons.get_impand on toconsumer.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
- call get(t): auditor → cons.get_imp
- Waiting: auditor — blocked inside consumer.get until 13 ns
- Data back at 13 ns
The producer's
puthas just stored #1.getpops 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.gethands #1 out, and the auditor'sget, called at 0 ns, returns.Follow the calls, values and waits
- return #1: cons.get_imp → auditor
- Released: auditor
- 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
- data #1: auditor → resp.tr_imp
- Waiting: auditor — inside responder.transport until 15 ns
- Response out at 15 ns
After 2 ns, #101 comes back in
rspat 15 ns, andtransportreturns.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
- return #101: resp.tr_imp → auditor
- 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.
| Connection | Form | Written in | In the example |
|---|---|---|---|
| Port to port | child.port.connect(parent_port) | The parent | Lifting put_port out of the agent and the env |
| Port to export or imp | a.port.connect(b.export) | The common parent of siblings | src.put_port.connect(snk.put_export) |
| Export to export or imp | parent_export.connect(child.export) | The parent | put_export.connect(cons.put_imp) |
| Export to port | Not allowed | The fault below | |
| Imp to anything | Not allowed | An 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.
- Bind once
When end of elaboration starts,
resolve_bindingswalks the five links andset_if(0)stores the one imp it found in the port'sm_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
putskip the intermediate exports.Follow the calls, values and waits
- data m_if = cons.put_imp: resolution, before end of elaboration → uvm_blocking_put_port
- Hop 1: the port10 ns (illustrative)
At 10 ns the producer calls
put_port.put(#1). The port'sputwas written by theUVM_BLOCKING_PUT_IMP(this.m_if, T, t)macro: it just callsm_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
- call put(#1): producer.run_phase → uvm_blocking_put_port
- Waiting: producer.run_phase — inside put(#1) until 13 ns
- Hop 2: the imp10 ns (illustrative)
m_ifis declared with the base type, so dispatch runs the imp'sput, written byUVM_BLOCKING_PUT_IMP(m_imp, T, t): it callsm_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
- call put(#1): uvm_blocking_put_port → uvm_blocking_put_imp #(pkt, consumer)
- Hop 3: your code10 to 13 ns (illustrative)
m_impis aconsumerhandle, soconsumer::putruns. It waits 3 ns and stores #1 in its own queue, still in the producer's thread.More context
The
#3is the only delay in the chain, and the producer's thread is the one that waits it.Follow the calls, values and waits
- call put(#1): uvm_blocking_put_imp #(pkt, consumer) → consumer::put
- Waiting: producer.run_phase — inside consumer::put until 13 ns
- Released: producer.run_phase
- Return13 ns (illustrative)
Each task returns to its caller, and the producer prints
put(#1) returnedat 13 ns. The port and the imp added no queue and no process; the only storage is the consumer's ownstore.More context
Trace lines 3 and 4.
puthas 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
- 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.
| Mistake | What this library reports | Severity |
|---|---|---|
| No connection for a port | connection count of 0 does not meet required minimum of 1, once per port in the chain | UVM_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 port | UVM_ERROR, then fatal |
| One put port with the default maximum reaching two imps | connection count of 2 exceeds maximum of 1 | UVM_ERROR, then fatal |
Skipping a level: test connects prod.put_port to cons.put_imp | Nothing by default; the call works | Warning 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.
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 : 0Check yourself
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
- Two environments that never talk Fix · Easy