In short: A UVM monitor uses a single uvm_analysis_port to broadcast every reconstructed transaction to any number of subscribers (scoreboard, coverage collector, loggers) through a one-to-many write() call, and a subscriber that needs to buffer those transactions pulls them from a uvm_tlm_analysis_fifo instead of acting on them immediately.
Of all the TLM building blocks in UVM, the analysis port is the one that separates a tangled testbench from a clean, reusable one. It is how passive components observe the DUT without ever being wired to each other. If you have interviewed for a verification role, you have almost certainly been asked to "explain how a monitor talks to the scoreboard and the coverage collector at the same time" — and the honest answer is: it doesn't talk to them, it broadcasts, and they choose to listen.
Why a normal TLM port isn't enough
A standard UVM blocking port such as uvm_blocking_put_port is a strict 1-to-1 connection: exactly one port, one export, one implementation. That is perfect for a driver pulling from a sequencer, where there is a single consumer. But a monitor sits in the middle of the testbench and typically feeds three or four independent consumers who must not know about each other. Connecting a 1-to-1 port to two exports throws a build-time error.
The analysis port solves this with a different contract. An uvm_analysis_port can be connected to zero, one, or many uvm_analysis_imp (or analysis FIFO) targets. When the producer calls ap.write(txn), UVM iterates its subscriber list and invokes the write() method of every connected subscriber in turn. The call is non-blocking and void — it never consumes simulation time and never returns a value. This is the architectural key: the monitor fires and forgets, so adding a tenth coverage collector cannot slow down or stall the monitor.
Analysis write() is a void function, not a task. It cannot block, cannot #delay, and must complete in zero time. If a subscriber needs to wait, it must queue the transaction — that is precisely the job of the analysis FIFO.
The broadcast picture
connect() in the env wires them up.Declaring and writing from the monitor
Inside the monitor you parameterize the port with the transaction type and call write() once the item is reconstructed from pin wiggles:
class apb_monitor extends uvm_monitor;
uvm_analysis_port #(apb_txn) ap;
function void build_phase(uvm_phase phase);
ap = new("ap", this);
endfunction
task run_phase(uvm_phase phase);
forever begin
apb_txn t = collect_transaction(); // samples the interface
ap.write(t); // broadcast to all listeners
end
endtask
endclass
Note there is only one port even though three components will receive the data. The monitor is completely decoupled: you can drop it into a block-level or SoC-level environment unchanged.
The coverage collector: subscribe directly
A coverage collector does not need to buffer anything — it samples a covergroup the instant a transaction arrives. The idiomatic choice is uvm_subscriber, which already contains an analysis_export and asks you to implement write():
class apb_cov extends uvm_subscriber #(apb_txn);
apb_txn tr;
covergroup cg; coverpoint tr.addr; coverpoint tr.cmd; endgroup
function new(string n, uvm_component p); super.new(n,p); cg=new(); endfunction
function void write(apb_txn t); tr=t; cg.sample(); endfunction
endclass
The scoreboard: buffer with an analysis FIFO
The scoreboard is different. It must compare a DUT output against a reference, and the two streams rarely arrive on the same clock. If the scoreboard tried to act inside write() it would be forced to do all its work in zero time — impossible when it needs to wait for the matching expected item. The clean solution is uvm_tlm_analysis_fifo, an unbounded FIFO that already provides an analysis_export on its input side and a blocking get() on its output side.
class apb_sb extends uvm_scoreboard;
uvm_tlm_analysis_fifo #(apb_txn) act_fifo, exp_fifo;
task run_phase(uvm_phase phase);
apb_txn a, e;
forever begin
exp_fifo.get(e); // blocks until predictor pushes
act_fifo.get(a); // blocks until monitor pushes
if (!a.compare(e)) `uvm_error("SB", "mismatch")
end
endtask
endclass
An analysis FIFO is the adapter between the non-blocking analysis world and the blocking get() world. Its write() side accepts broadcasts instantly; its get() side lets a consuming thread wait. It is unbounded, so write() can never stall the monitor.
write() from the scoreboard thread that waits on get().Port, imp, or FIFO — which do I use?
| Need | Use | Why |
|---|---|---|
| Act immediately, no waiting (coverage) | uvm_subscriber / analysis_imp | You implement write(); runs in zero time |
| Buffer and consume at your own pace (scoreboard) | uvm_tlm_analysis_fifo | Non-blocking in, blocking get() out |
| Fan out to many targets | uvm_analysis_port | One write() reaches all subscribers |
| A component has two independent inbound streams | `uvm_analysis_imp_decl macro | Creates write_a(), write_b() suffixed methods |
Watching the broadcast in motion
ap.write(t) delivers the same transaction handle to every subscriber in the same delta cycle.Gotchas that cost real debug time
Handle vs. copy. write() passes the object handle, not a deep copy. If the monitor reuses or modifies that object on the next loop, every subscriber sees the change. Always allocate a fresh transaction each iteration, or clone() before writing. This is the single most common cause of phantom scoreboard mismatches in Synopsys VCS and Cadence Xcelium runs alike.
Forgotten connect. If you never call mon.ap.connect(sb.act_fifo.analysis_export) in the env's connect_phase, there is no error — the transaction simply goes nowhere, and coverage silently reads 0%. Siemens Questa's UVM resource reporting can help, but the habit of checking fan-out counts saves hours.
For teams building a full reusable UVC, our partner Avecas design-services team treats analysis-port fan-out as the contract that keeps block- and SoC-level reuse honest.
FAQ
Is uvm_analysis_port blocking?
No. Its write() is a void function that executes in zero simulation time. If a consumer needs to wait, put a uvm_tlm_analysis_fifo between them.
What is the difference between an analysis port and an analysis export?
A port is the producer side that initiates write(); an export (or imp) is the consumer side that provides the write() implementation. You connect a port to one or more exports.
Why use an analysis FIFO instead of implementing write() directly in the scoreboard?
Because the scoreboard often must wait for a matching reference transaction before comparing, and write() cannot block. The FIFO buffers incoming items so a separate thread can get() them when ready.
Can one component receive two analysis streams?
Yes. Use the `uvm_analysis_imp_decl(_suffix) macro to generate distinct write_suffix() methods, so a single component can host multiple independent analysis imps.
Want to wire monitors, scoreboards and coverage into a production-grade UVC with guided labs? Explore the Verification track on our CourseTron courses page and build a full APB/AXI environment from scratch.
Keep going: understand the consumer side in our deep dive on the UVM scoreboard, then see how items originate in UVM sequences and the sequencer. For live cohort-based training, our sister brand ChipXpert VLSI training runs the same flow with mentor support.