You are moving the DMA scoreboard into a small checker component so the subsystem env can reuse it. You add an export to the checker, write the env's connect line the wrong way round, and the run dies at time zero before a single clock. Reproduced in a minimal test (tlm_export_port_test in tlm_tb.sv), the log reads:
UVM_ERROR @ 0: uvm_test_top.exp [Connection Error] Cannot connect exports to ports Try calling port.connect(export) instead. (You attempted to bind this export to uvm_test_top.prt).
UVM_ERROR @ 0: uvm_test_top.exp [Connection Error] connection count of 0 does not meet required minimum of 1
UVM_FATAL @ 0: reporter [BUILDERR] stopping due to build errorsThe message even tells you the fix, but only for that one link. A chain that crosses three levels of hierarchy has a direction at every hop. Which way does each connect() point, who calls it, and which mistakes does the library catch?
A call travels from a port to an imp. Every connect() is called on the end nearer the caller and takes the end nearer the implementer as its argument: requester.connect(provider). Inside a hierarchy that gives three moves. A port goes up: a child's port connects to its parent's port, so the parent can offer the child's call to the outside. A port goes across: it connects to a sibling's export or imp. An export goes down: a parent's export connects to a child's export or imp. The imp at the bottom runs the method and connects to nothing.
Picture internal mail in an office campus. A letter goes from your desk up to your building's outbox (port to port), across the road to the other building's mail room (port to export), and down to the recipient's desk (export to imp). Nobody walks a letter from the mail room back up into someone's outbox.
Read who calls each connect(). The agent connects its children (mon.ap to ap, driver to sequencer). The env connects its children (in_agt.ap to chk.in_export). The checker connects its own export to its child. Each component wires only what it owns, inside its own connect_phase, and because connect_phase runs bottom-up, every child is wired before its parent looks at it.
// snippet: from the course DMA bench
class apb_agent extends uvm_agent;
`uvm_component_utils(apb_agent)
apb_sequencer sqr;
apb_driver drv;
apb_monitor mon;
uvm_analysis_port #(apb_item) ap; // the agent's public output
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase); // reads is_active from config_db
mon = apb_monitor::type_id::create("mon", this);
if (get_is_active() == UVM_ACTIVE) begin
sqr = apb_sequencer::type_id::create("sqr", this);
drv = apb_driver::type_id::create("drv", this);
end
endfunction
function void connect_phase(uvm_phase phase);
ap = mon.ap;
if (get_is_active() == UVM_ACTIVE)
drv.seq_item_port.connect(sqr.seq_item_export);
endfunction
endclassThe line ap = mon.ap does not connect anything. It makes the agent's ap variable a second name for the monitor's port object, so apb.ap and apb.mon.ap are the same object. That is why no ap row appears under the agent in the topology table, and why debug output names the port uvm_test_top.env.in_agt.mon.ap. It is cheap and common. Its one requirement is timing: the assignment must run before the env uses apb.ap, which the bottom-up connect_phase guarantees.
// snippet: from the course DMA bench
class dma_env extends uvm_env;
`uvm_component_utils(dma_env)
apb_agent apb;
axis_agent in_agt, out_agt;
dma_scoreboard sb;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
// The output side is observed only: the DUT drives it.
uvm_config_db#(uvm_active_passive_enum)::set(this, "out_agt", "is_active", UVM_PASSIVE);
apb = apb_agent::type_id::create("apb", this);
in_agt = axis_agent::type_id::create("in_agt", this);
out_agt = axis_agent::type_id::create("out_agt", this);
sb = dma_scoreboard::type_id::create("sb", this);
endfunction
function void connect_phase(uvm_phase phase);
apb.ap.connect(sb.apb_export);
in_agt.ap.connect(sb.in_export);
out_agt.ap.connect(sb.out_export);
endfunction
endclassEach line is a port connecting across to a sibling's imp: apb and sb are both children of the env. A port may also be connected to more than one imp, and an imp may be reached by more than one port; analysis broadcast covers that fan-out and fan-in.
Instead of borrowing the monitor's object, the agent can own a real port and pass the monitor's port up into it. This is the textbook child-port-to-parent-port move:
// snippet: inside axis_agent, a pass-through port instead of ap = mon.ap
uvm_analysis_port #(axis_item) ap;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
ap = new("ap", this); // the agent's own port object
mon = axis_monitor::type_id::create("mon", this);
// sqr and drv as before
endfunction
function void connect_phase(uvm_phase phase);
mon.ap.connect(ap); // child port up to parent port
if (get_is_active() == UVM_ACTIVE)
drv.seq_item_port.connect(sqr.seq_item_export);
endfunction
| Pass-through port | |
|---|---|---|
Port objects | One: the monitor's | Two: the monitor's and the agent's |
In the topology | Only |
|
Can the agent add its own traffic to | Only by writing to the monitor's port | Yes, it can call |
Typical failure | Using | Forgetting |
// snippet
class dma_checker extends uvm_component;
`uvm_component_utils(dma_checker)
uvm_analysis_export #(axis_item) in_export; // the checker's public input
dma_scoreboard sb;
function new(string name, uvm_component parent);
super.new(name, parent);
in_export = new("in_export", this);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
sb = dma_scoreboard::type_id::create("sb", this);
endfunction
function void connect_phase(uvm_phase phase);
in_export.connect(sb.in_export); // parent export down to child imp
endfunction
endclass
// env connect_phase: in_agt.ap.connect(chk.in_export); // port across to sibling exportMistake | When it is caught | What you see |
|---|---|---|
Different transaction types ( | Compile time | A type error on the |
Different families with the same type (a put port into a get imp) | At the | Connection Error: does not provide the complete interface |
| At the | Connection Error: Cannot connect exports to ports |
| At the | Connection Error: Cannot call an imp port's connect method |
Provider offers only part of the interface (a full | At the | Connection Error: does not provide the complete interface |
Connecting at or after end_of_elaboration | At the call | Warning |
Wrong hierarchy (a port into its own parent's export) | At the call, but only if enabled | Connection Warning; silent by default |
Too few or too many connections | Start of end_of_elaboration | Connection Error from |
Two rows surprise people. The hierarchy checks (up, across, down) exist in the library, but a port silences its own Connection Warning unless check_connection_relationships is set for it in config_db, and analysis ports skip the check entirely. So a wrong-shaped connection usually works without a word, which is one more reason to follow the rules by habit. Connection counts are checked last, by resolve_bindings, in TLM1 channels and port cardinality. Any UVM_ERROR raised before end of elaboration finishes makes the root stop with BUILDERR, as in the opening log.
The opening log came from a copy of the bench with one deliberate line in a test's connect_phase: exp.connect(prt), an export connected into a port. The library refuses the link on the spot, records nothing, and the root ends the run at time 0 because a build error occurred. The fix is the reverse call, prt.connect(exp), so that the port, the caller's end, is the one that connects.
A team switched their stream agent to the pass-through style above, so they could add the agent's own sideband traffic to ap. The new build_phase created ap. The connect_phase kept only the driver line; mon.ap.connect(ap) was lost in the edit. The env still connected in_agt.ap and out_agt.ap to the scoreboard, and both connections were perfectly legal.
At run time each monitor wrote into its own mon.ap, which now had no subscribers, and the agent's ap had subscribers but no one writing into it. No elaboration error: an analysis port with zero subscribers is legal, and so is one with nobody calling it. The APB stream still worked, so the scoreboard saw KEY writes and nothing else. What saved them was the bench's own check: check_phase reported SB_EMPTY, no words were compared. Without that check the test would have passed. A one-line env.in_agt.mon.ap.debug_connected_to() at end of elaboration, shown in imp_decl vs FIFOs, prints Connected implementations: none and finds it in a minute.
chk.in_export.connect(in_agt.ap) in the env. What happens, and what is the fix?connect() reports Cannot connect exports to ports and ignores the link, and the root stops with BUILDERR at end of elaboration. The call must start from the caller's end: in_agt.ap.connect(chk.in_export).
ap = mon.ap. A new env author connects in_agt.ap to the scoreboard in the env's build_phase. What goes wrong?In the env's build_phase the agent object has just been created, but its own build_phase (which runs after the env's, top-down) has not built the monitor, and its connect_phase has not assigned ap. So in_agt.ap is a null handle and the call fails with a null-object error. Connections belong in connect_phase, where children are fully built and wired first.
dma_checker with three exports. Which components make which connect() calls?The checker connects its own exports down to its scoreboard's imps (apb_export.connect(sb.apb_export) and so on). The env connects each agent's port across to the checker's matching export. Neither reaches into the other's children.
Next: the analysis links never wait for anyone. In put, get, peek and transport you will meet the families where the receiver controls the pace, and see a put() that takes 20 ns to return.