TLM Basics & Analysis Ports
The monitor and scoreboard from The Monitor & Subscriber and The Scoreboard already used TLM without this section pausing to name the mechanism: uvm_analysis_port, uvm_analysis_imp, and the agent.monitor.ap.connect(scoreboard.ap_imp) line in the environment's connect_phase. This page names what was already built, then opens up the wider TLM family — analysis ports are only one of several patterns UVM offers for components to talk to each other without holding direct references.
The problem TLM solves
Without it, the monitor would need a handle to the scoreboard directly, and the scoreboard's type would leak into the monitor's own code:
// Don't do this — monitor now depends directly on the scoreboard's existence and type
class fifo_monitor extends uvm_monitor;
fifo_scoreboard sb; // tight coupling: can't reuse this monitor without a fifo_scoreboard
// ...
endclass
Add a coverage collector later (as The Monitor & Subscriber did with fifo_coverage), and the monitor needs editing again just to notify it too. TLM (Transaction-Level Modeling) removes that coupling: components talk through typed ports rather than direct object references, so neither side needs to know who — or how many — is on the other end.
Three roles: port, export, imp
Every TLM connection in UVM is built from the same three roles, regardless of which specific pattern (analysis, put/get, FIFO, or socket — the subject of the rest of this section) is being used:
- Port — declared by whichever component initiates the connection (a monitor broadcasting, a sequence requesting an item).
- Imp ("implementation") — declared by whichever component actually implements the behavior (a scoreboard's
write(), a driver's handling of a request). - Export — a pass-through, used when a connection needs to cross an intermediate component that neither initiates nor implements the behavior itself, just forwards it along.
Analysis ports, covered next, only ever need the first two — no export — which is part of what makes them the simplest TLM pattern to start with.
Analysis ports and imps, recapped
class fifo_monitor extends uvm_monitor;
`uvm_component_utils(fifo_monitor)
uvm_analysis_port #(fifo_txn) ap;
// ...
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this); // ports/exports are new()'d, not factory-created
endfunction
// ...
endclass
class fifo_scoreboard extends uvm_scoreboard;
`uvm_component_utils(fifo_scoreboard)
uvm_analysis_imp #(fifo_txn, fifo_scoreboard) ap_imp;
// ...
function void write(fifo_txn txn); // called automatically on every ap.write()
// ...
endfunction
endclass
The defining trait of an analysis port, compared to every other TLM pattern in this section: it's one-to-many and fire-and-forget. ap.write(txn) broadcasts to zero, one, or any number of connected imps — the monitor never finds out how many are listening, or whether the call "succeeded" in any meaningful sense, because broadcast has no concept of failure. Connecting a coverage collector later, as The Monitor & Subscriber did, is one more .connect() call in the environment; neither the monitor nor the scoreboard classes need to change. When more than one imp is connected to the same port, a single ap.write(txn) call runs each connected imp's write() in the order the .connect() calls wired them up, one after another within that same call — not in parallel, and not in some unspecified order.
The export role, worked out
The three-role description above named exports but didn't show one. Suppose fifo_monitor's ap sits inside an agent, and the scoreboard that needs its data lives one level up in the environment — the agent itself doesn't implement write(), it just needs to pass the monitor's broadcast up and out:
class fifo_agent extends uvm_agent;
fifo_monitor mon;
uvm_analysis_port #(fifo_txn) ap; // agent-level pass-through, re-declared here
function void connect_phase(uvm_phase phase);
mon.ap.connect(ap); // wires the monitor's port straight through the agent's own port
endfunction
endclass
Rather than the environment reaching two levels down into fifo_agent.mon.ap directly (breaking encapsulation — the environment shouldn't need to know the agent contains a monitor at all), the agent exposes its own port at its own level, and forwards the monitor's broadcast through it. The environment then only ever connects to agent.ap, never to agent.mon.ap — the same port → port → export → export → imp chaining rule applies generally: each hop up the hierarchy re-declares a pass-through port/export at that level, and the chain must still end at an actual imp somewhere for the connection to be complete.
What the rest of this section covers
Analysis ports are the right tool for exactly one shape of problem: broadcasting observed data to any number of interested listeners. Plenty of TLM communication doesn't fit that shape — a single producer talking to a single consumer, with the consumer's readiness actually mattering. The sequencer↔driver handshake from The Sequencer & Sequence — get_next_item()/item_done() — is exactly this kind of one-to-one exchange, built on the same underlying put/get family of ports this section covers next, just pre-packaged by uvm_driver/uvm_sequencer so a testbench author never has to wire up the raw ports directly. The next four pages build that picture out in full: blocking put/get, non-blocking put/get, the uvm_tlm_fifo that buffers between mismatched producer/consumer rates, and TLM sockets for the cases plain ports/exports/imps don't cleanly cover.
What's next
Blocking put and get ports — the simplest one-to-one request/response pattern, and the one closest in shape to the sequencer/driver handshake already familiar from earlier in this section.