UVM
Analysis Ports in Monitors
How a monitor delivers what it observed — the analysis port: a one-to-many, non-blocking broadcast (publish-subscribe) that fans each transaction out to every subscriber at once, decoupling the monitor from the scoreboard, coverage, and loggers that consume it.
Monitors · Module 13 · Page 13.4
The Engineering Problem
The monitor reconstructs transactions (Module 13.2) and checks the protocol (Module 13.3). Now it must deliver what it observed to the rest of the testbench — and many components want it: the scoreboard (to check correctness), coverage (to measure what's been exercised), loggers, predictors, performance trackers. The naive approach — the monitor holding a handle to each consumer and calling them directly — couples the monitor to every component that wants its data: add a coverage collector and you edit the monitor; reuse the monitor in a different environment with different consumers and you rewrite it. That's backwards — the monitor's job is to observe, not to know its audience. The problem this chapter solves is delivery: how does one monitor feed its observations to any number of independent consumers without knowing or caring who they are?
The answer is the analysis port — a one-to-many, non-blocking, broadcast connection (the publish-subscribe pattern, TLM analysis). The monitor declares a uvm_analysis_port and publishes each observed transaction by calling ap.write(tr); that single call fans the transaction out to every connected subscriber — synchronously, in zero time, in turn — and the monitor neither blocks nor knows nor cares how many subscribers there are (zero, one, or many). This decouples the monitor (the producer) from the scoreboard, coverage, and loggers (the consumers): subscribers can be added or removed without touching the monitor. It is the opposite of the driver's seq_item_port — which is point-to-point, blocking, pull; analysis is fan-out, non-blocking, push. This chapter is the analysis port: the broadcast model, the write() semantics, the decoupling it buys, and the multi-stream subscription bug that hides in a scoreboard.
How does a monitor deliver its observations through an analysis port — a one-to-many, non-blocking broadcast that fans each transaction out to every subscriber at once — and how does that decouple the monitor from the scoreboard, coverage, and loggers that consume it?
Motivation — why broadcast, not direct calls
The analysis port's broadcast model exists to decouple the monitor from an open-ended, changing set of consumers. The reasons direct coupling fails:
- The audience is many and changing. A monitor's observation feeds the scoreboard, coverage, loggers, predictors — a set that differs per environment and grows over a project. The monitor can't hard-code handles to all of them and stay reusable.
- The producer shouldn't know its consumers. The monitor's job is to observe and publish — who listens is not its concern. Coupling it to specific consumers makes it specific to one environment; broadcast makes it universal.
- Adding a subscriber must not touch the monitor. Want coverage on an existing environment? With broadcast, you add a subscriber and connect it — the monitor is unchanged. With direct calls, you'd edit the monitor to call the new consumer — a modification to verified code.
- Delivery must not block observation. The monitor runs a tight observe-loop; it can't stall waiting for a slow consumer.
write()is non-blocking (a function, zero-time) — the monitor publishes and moves on, never held up by a subscriber. - Zero subscribers must be legal. A monitor with nothing connected to its port must still work —
write()to an unconnected port is legal and does nothing. This is the ultimate decoupling: the monitor behaves identically whether anyone is listening.
The motivation, in one line: a monitor's observation is needed by a many, changing, environment-specific set of consumers, so it broadcasts through an analysis port — publishing to whoever subscribes, non-blocking and unaware of its audience — which keeps the monitor reusable and unmodified as consumers are added, removed, or swapped.
Mental Model
Hold the analysis port as a radio station broadcasting to whoever tunes in:
The monitor is a radio station: it broadcasts its observation on a frequency, and any number of receivers — scoreboard, coverage, loggers — tune in. The station doesn't know or care who's listening; adding a listener never changes the station. Picture the monitor as a broadcast transmitter. Each time it observes a transaction, it transmits it on its frequency (calls
write()on its analysis port). Out in the world, receivers (subscribers) are tuned to that frequency: the scoreboard receives it to check it, coverage receives it to measure it, a logger receives it to record it. The defining properties follow from the broadcast nature. The station transmits to all at once — one transmission reaches every tuned receiver simultaneously (onewrite()fans out to all subscribers). The station doesn't know its audience — it broadcasts blindly; whether ten receivers or zero are tuned in, the station transmits the same way (a port with no subscribers still acceptswrite(), harmlessly). Adding a receiver changes nothing about the station — a new listener just tunes in; the transmitter is untouched (add a coverage collector, connect it, the monitor is unmodified). And the station never waits for its listeners — it transmits and moves on; it can't be held up by a slow receiver (write()is non-blocking — a receiver that needs to think must record and process later, not stall the broadcast). Contrast this with a phone call — the driver'sseq_item_port— which is point-to-point (one caller, one callee), engaged (both parties on the line), and blocking (you wait for the other end). The analysis port is not a phone call; it's broadcast radio.
So the analysis port is broadcast radio: the monitor transmits to whoever tunes in, all at once, blind to its audience, never waiting, and unchanged when listeners come and go. The scoreboard, coverage, and loggers are receivers that subscribe; the monitor publishes. This is the publish-subscribe pattern, and it's exactly what keeps the monitor reusable across environments with different listeners — the one thing a monitor must not do is know its audience.
Visual Explanation — one port, many subscribers
The defining picture is the fan-out: one analysis port on the monitor, broadcasting each transaction to many independent subscribers at once.
The figure shows the one-to-many fan-out that defines the analysis port. The monitor has a single analysis port, and each observed transaction is published with one call — write(tr). That call fans out: the analysis port broadcasts the transaction to every connected subscriber at once — the scoreboard (which checks it against expected), coverage (which measures which scenarios it exercised), and the logger (which records it). The crucial reading is the symmetry of ignorance: the subscribers are independent and unaware of each other — coverage doesn't know the scoreboard exists; and the monitor is unaware of the subscribers — it just publishes to the port, which handles the fan-out. This mutual decoupling is the value: the warning-colored analysis port in the middle is the only thing that knows about both sides, and it connects them without either side knowing the other. The practical payoff is in the wiring: adding a subscriber (say, a performance tracker) means creating it and connecting it to the port in the connect_phase — the monitor is never touched. Removing one is just deleting the connection. The monitor's code is identical whether it feeds one subscriber or five or none. The diagram is the publish-subscribe pattern made concrete: one publisher (the monitor), many subscribers (scoreboard, coverage, logger), connected through a broadcast port that decouples them — the structural reason a monitor is reusable across environments that consume its data differently.
RTL / Simulation Perspective — write() and the non-blocking rule
In code, the monitor publishes with ap.write(tr), and each subscriber receives via a write() method. The key rule: write() is a function (zero-time), so a subscriber that needs time must defer the work. The code shows both sides.
// === MONITOR: declare an analysis port, broadcast each observed transaction ===
class my_monitor extends uvm_monitor;
uvm_analysis_port #(bus_item) ap; // ONE port — broadcasts to all subscribers
function new(string n, uvm_component p); super.new(n,p); ap = new("ap", this); endfunction
task run_phase(uvm_phase phase);
forever begin
bus_item tr = reconstruct(); // observe + reconstruct (13.2)
ap.write(tr); // PUBLISH: non-blocking, fans out to ALL subscribers
end // monitor does NOT wait, does NOT know who listens
endtask
endclass
// === SUBSCRIBER (coverage): write() runs in ZERO time — sample and return ===
class cov_sub extends uvm_subscriber #(bus_item); // has a built-in analysis_export
function void write(bus_item t); // called by the port's broadcast — MUST NOT block
cg.sample(); // zero-time work is fine (sample coverage)
endfunction
endclass
// === SUBSCRIBER needing TIME: defer via an analysis FIFO, process in run_phase ===
class sb extends uvm_scoreboard;
uvm_tlm_analysis_fifo #(bus_item) fifo; // write() pushes here (non-blocking)...
task run_phase(uvm_phase phase);
forever begin
bus_item t; fifo.get(t); // ...and a separate task consumes (CAN block) (12-style)
compare_against_expected(t);
end
endtask
endclass
// === CONNECT: port → export, in connect_phase. Monitor untouched when subscribers change ===
function void connect_phase(uvm_phase phase);
mon.ap.connect(cov.analysis_export); // add/remove subscribers HERE, not in the monitor
mon.ap.connect(sb.fifo.analysis_export);
endfunctionThe code shows the publish/subscribe mechanics and the one rule that trips people. The monitor declares one uvm_analysis_port and, per observed transaction, calls ap.write(tr) — a non-blocking publish that fans out to all subscribers; the monitor does not wait and does not know who listens. A subscriber receives via a write() method — and here's the rule: write() is a function, so it runs in zero time and must not block. Zero-time work (coverage's cg.sample()) is fine inside write(). But a subscriber that needs time — the scoreboard, which may wait for a matching transaction — cannot do that work in write(); it pushes to a uvm_tlm_analysis_fifo (a non-blocking write into a fifo) and processes in a separate run_phase task (which can block on fifo.get()). The connection happens in connect_phase: mon.ap.connect(subscriber.analysis_export) — port → export, source → destination — and this is where subscribers are added or removed, never in the monitor. The shape to carry: the monitor publishes non-blocking to a broadcast port; subscribers receive in zero-time write() methods and defer any time-consuming work to a fifo + run_phase; and the wiring lives in connect_phase, decoupled from the monitor. The write()-must-not-block rule is the one place the broadcast model's non-blocking nature becomes a concrete coding constraint.
Verification Perspective — decoupling and disambiguating streams
The analysis port buys decoupling — but using it well means connecting subscribers without touching the monitor and, when a subscriber listens to multiple monitors, telling the streams apart.
The figure shows the two practical faces of using analysis ports well. On the left, decoupled subscription: adding a new subscriber (coverage) to a live environment touches only the connect_phase wiring — the monitor is unchanged, and the existing subscribers are unaffected. This is the decoupling in action: new consumers cost nothing to existing code. On the right, the subtlety that bites when a subscriber listens to more than one monitor: a scoreboard typically subscribes to two monitors — an input monitor (the expected stream) and an output monitor (the actual stream) — and it must tell them apart. But a component has one write() method by default, so both streams would arrive at the same write() and the scoreboard couldn't tell which monitor a transaction came from. The fix is a separate analysis imp per stream, declared with the uvm_analysis_imp_decl macro: it generates distinct imps that route to distinctly-named methods — write_expected (imp #1, from the input monitor) and write_actual (imp #2, from the output monitor) — so each stream arrives at its own method and the scoreboard knows the source. The verification insight is that the analysis port's broadcast model is deliberately source-blind — a write() doesn't carry "who sent this" — so when a subscriber needs to distinguish sources, it must provide distinct entry points (distinct imps), one per stream. This is the one place the broadcast model's simplicity (everything to one write()) needs explicit help, and it's the source of a classic scoreboard bug (the DebugLab). The figure's two halves are the everyday analysis-port skills: add subscribers without touching the monitor, and give each incoming stream its own imp so a multi-source subscriber can tell them apart.
Runtime / Execution Flow — a broadcast in zero time
At run time, write() is a synchronous, zero-time fan-out: the monitor calls it, and every subscriber's write() runs in turn, in the same time-step, before the monitor proceeds. The flow shows the broadcast.
The flow is the broadcast's zero-time, synchronous nature. Publish (step 1): the monitor calls ap.write(tr) once — a single non-blocking call to the port. Fan out (step 2): the port invokes each connected subscriber's write() method in turn — in connection order, all within the same simulation time-step. Return immediately (step 3): each subscriber does zero-time work only (sample coverage, push to a fifo) and returns — never blocking, because write() is a function. Continue (step 4): when the last subscriber's write() returns, control comes back to the monitor, which proceeds to observe the next transaction. The runtime insight is that the entire broadcast is instantaneous in simulation time: from the monitor's perspective, ap.write(tr) is a single, immediate call that returns once all subscribers have seen the transaction — no time has elapsed, and no subscriber has held it up. This is why write() must not block: if a subscriber could stall inside write(), it would freeze the monitor mid-observation — so the non-blocking rule is what preserves the monitor's independence from its consumers (a subscriber's slowness can never affect the monitor's timing). A subscriber that needs to think (the scoreboard) defers: its write() pushes to a fifo in zero time and returns, and a separate run_phase task does the time-consuming work off the broadcast path. The flow is the publish-subscribe broadcast in operation: one publish, synchronous fan-out to all, zero time, no blocking — the monitor touches every subscriber at once and moves on, uncoupled from how long any of them ultimately takes to process what they received.
Waveform Perspective — the fan-out happens in one time-step
The broadcast's simultaneity is visible on a timeline: at the cycle the monitor publishes, every subscriber receives the transaction in the same time-step. The waveform shows the zero-time fan-out.
One write() at the publish cycle fans out to every subscriber in the same time-step
12 cyclesThe waveform shows the broadcast's simultaneity. The monitor reconstructs a transaction from the bus (data=5E at the valid cycle) and publishes it: ap_write pulses at the publish cycle. In that same cycle, every subscriber receives it — sb_recv, cov_recv, and log_recv all pulse at the same cycle — because write() fans out synchronously, in zero time. The crucial visual is the vertical alignment: the publish pulse and all the receive pulses sit on the same cycle, no subscriber lags — the transaction reaches all of them at once. There's no staggering, no queue delay on the broadcast itself: the fan-out is instantaneous. (If the scoreboard defers processing via a fifo, that later work happens off this path — but the receipt into the fifo is still at the publish cycle.) The second transaction (data=C1) shows the same pattern: one publish, all subscribers simultaneously. The picture to carry: a publish is a single, simultaneous, zero-time fan-out — the monitor's one write() touches every subscriber in the same instant — and the monitor's loop timing is unaffected by how many subscribers there are. Reading a waveform for analysis-port behavior — does every subscriber receive at the publish cycle? does the publish timing depend on the subscriber count? — confirms the broadcast is simultaneous and the monitor is decoupled: adding or removing a subscriber would change which receive signals exist, never the timing of ap_write or the monitor's loop.
DebugLab — the scoreboard that couldn't tell its two streams apart
A scoreboard mixing expected and actual because both monitors hit one write()
A scoreboard subscribed to two monitors — an input monitor (expected transactions) and an output monitor (actual transactions from the DUT) — and its checks were nonsense: it compared transactions that shouldn't be paired, reported mismatches on correct data, and missed real ones. Inspection showed the scoreboard's single write() method receiving a jumbled stream — expected and actual transactions interleaved with no way to tell which was which. The scoreboard couldn't distinguish its two sources.
The scoreboard had one analysis export / write() method, and both monitors were connected to it. Since a write() call carries no source identity (the broadcast is source-blind), the two streams merged into one write() with no way to separate them:
✗ ONE write() for TWO sources — streams merge, source is lost:
class sb extends uvm_scoreboard;
uvm_analysis_imp #(bus_item, sb) imp; // ONE imp → ONE write()
function void write(bus_item t); // BOTH monitors arrive here — which is which?
... // expected and actual interleaved, indistinguishable → bogus comparisons
// connect_phase:
in_mon.ap.connect(sb.imp); // both go to the same write()
out_mon.ap.connect(sb.imp); // source identity is LOST
✓ a SEPARATE imp per stream via uvm_analysis_imp_decl — distinct write methods:
`uvm_analysis_imp_decl(_expected) // generates analysis_imp_expected + write_expected
`uvm_analysis_imp_decl(_actual) // generates analysis_imp_actual + write_actual
class sb extends uvm_scoreboard;
uvm_analysis_imp_expected #(bus_item, sb) exp_imp;
uvm_analysis_imp_actual #(bus_item, sb) act_imp;
function void write_expected(bus_item t); ... endfunction // input monitor → here
function void write_actual (bus_item t); ... endfunction // output monitor → here
// connect_phase:
in_mon.ap.connect(sb.exp_imp); // expected stream → write_expected
out_mon.ap.connect(sb.act_imp); // actual stream → write_actualThis is the multi-stream disambiguation bug — the classic scoreboard wiring mistake, rooted in a correct understanding of one analysis stream applied incorrectly to two. The analysis port's broadcast is deliberately source-blind: a write(t) delivers the transaction but not "who sent it" — which is fine when a subscriber listens to one source, but breaks when it listens to two. The scoreboard declared one analysis_imp (one write() method) and connected both monitors to it. So every transaction — expected from the input monitor and actual from the output monitor — arrived at the same write(), interleaved, with no field indicating its source. The scoreboard couldn't tell an expected from an actual, so its pairing logic compared the wrong things: bogus mismatches on correct data, missed real ones — the checking was meaningless. The fix is the uvm_analysis_imp_decl macro, which generates a distinct analysis imp (and a distinctly-named write method) per stream: declare _expected and _actual imps, giving two methods — write_expected and write_actual — then connect the input monitor to the expected imp and the output monitor to the actual imp. Now each stream arrives at its own method, the scoreboard knows the source of every transaction, and its pairing is correct. The general lesson, and the chapter's thesis: the analysis broadcast is source-blind — a write() doesn't carry who sent it — so a subscriber listening to multiple analysis streams must give each stream its own analysis imp (via uvm_analysis_imp_decl) to route them to distinct methods and tell them apart; a single write() cannot distinguish two sources, and merging them destroys the information the subscriber needs.
The tell is a multi-source subscriber whose logic compares or pairs the wrong items. Diagnose stream-merging:
- Count the sources and the imps. If a subscriber connects to more than one analysis port but has one
write()method, the streams are merging. - Check whether write() can tell the source. A single
write(t)carries no source identity; if the logic needs to know which monitor sent a transaction, one method can't provide it. - Look for uvm_analysis_imp_decl. A scoreboard listening to input and output monitors should declare a separate imp per stream; its absence is the bug.
- Trace a known expected and a known actual. If both land in the same method interleaved, the disambiguation is missing.
Give each incoming analysis stream its own entry point:
- One analysis imp per source. A subscriber listening to multiple monitors declares a distinct imp per stream with
uvm_analysis_imp_decl, routing each to its own write method. - Name the methods by stream. write_expected, write_actual — so the source is explicit at the entry point, not inferred.
- Connect each monitor to its matching imp. Input monitor to the expected imp, output monitor to the actual imp, in connect_phase.
- Keep write() non-blocking; defer with a fifo. Each per-stream write pushes to its fifo (or updates state) in zero time; time-consuming pairing happens in run_phase.
The one-sentence lesson: the analysis broadcast is source-blind — a write() carries the transaction but not who sent it — so a subscriber listening to multiple analysis streams must give each stream its own analysis imp via uvm_analysis_imp_decl, routing them to distinct write methods (write_expected, write_actual); a single write() cannot distinguish two sources, and merging them destroys the source identity the subscriber needs to pair correctly.
Common Mistakes
- Blocking inside write().
write()is a zero-time function; a subscriber that needs time must push to auvm_tlm_analysis_fifoand process inrun_phase, never stall insidewrite(). - One write() for multiple sources. A subscriber listening to several monitors needs a distinct analysis imp per stream (
uvm_analysis_imp_decl); onewrite()can't tell the sources apart. - Coupling the monitor to its consumers. The monitor publishes to a port and must not hold handles to specific subscribers; add and remove consumers in
connect_phase. - Connecting in the wrong direction. Analysis connects port → export (source → destination); reversing it means nothing is received.
- Assuming a subscriber must exist. A
write()to a port with no subscribers is legal and harmless; the monitor must work the same with zero listeners. - Doing heavy work on the broadcast path. Time-consuming processing inside
write()would stall the monitor; keep the broadcast path zero-time and defer.
Senior Design Review Notes
Interview Insights
An analysis port is a one-to-many, non-blocking, broadcast connection — the publish-subscribe pattern in UVM's TLM analysis. A monitor declares a uvm_analysis_port and publishes each observed transaction by calling write() on it; that one call fans the transaction out to every connected subscriber synchronously, in zero time, and the monitor neither blocks nor knows nor cares how many subscribers there are. It's how a monitor delivers its observations to the scoreboard, coverage, loggers, and any other consumers, all at once, while staying decoupled from them. The driver's seq_item_port is the opposite in every dimension. It's point-to-point — one port connects to one sequencer, not many. It's blocking — the driver calls get_next_item and waits, blocking until a transaction is available, which is essential because the driver must pace itself to the sequencer. And it's a pull — the consumer, the driver, pulls transactions from the producer. The analysis port is one-to-many instead of point-to-point, non-blocking instead of blocking, and push instead of pull — the monitor pushes a transaction out to all subscribers. The mental model captures it: the seq_item_port is a phone call — one caller, one callee, both engaged, you wait for the other end — while the analysis port is broadcast radio — the station transmits to whoever tunes in, all at once, never waiting, blind to its audience. The reason for the difference is the role: the driver needs paced, point-to-point flow control from its sequencer, while the monitor needs to fan its observation out to an open-ended set of independent consumers without coupling to any of them.
Because the monitor's observation is needed by a many, changing, environment-specific set of consumers, and the monitor must stay reusable and unmodified as that set changes. If the monitor held handles to specific consumers and called them directly, it would be coupled to exactly those consumers. Adding a coverage collector would mean editing the monitor to call it — modifying verified code. Reusing the monitor in a different environment with different consumers would mean rewriting it. That's backwards, because the monitor's job is to observe and publish, not to know its audience. The broadcast model decouples the producer from the consumers. The monitor just publishes to its analysis port, and subscribers connect to that port in the connect phase. Adding a subscriber means creating it and connecting it — the monitor is never touched. Removing one is just deleting a connection. The monitor's code is identical whether it feeds five subscribers, one, or none — a write to a port with no subscribers is legal and does nothing. This mutual ignorance is the value: the subscribers don't know about each other, and the monitor doesn't know about the subscribers; the port connects them without either side knowing the other. It's the publish-subscribe pattern, and it's what makes the monitor universal across environments that consume its data differently. The broadcast also keeps delivery non-blocking, so a slow consumer can never stall the monitor's observe loop. So the broadcast model exists to keep the monitor decoupled, reusable, and unaffected by how many consumers exist or what they do — exactly the properties direct coupling destroys.
Exercises
- Contrast the ports. List three ways an analysis port differs from a
seq_item_port(cardinality, blocking, direction). - Defer correctly. A scoreboard needs to wait for a matching transaction. Show why it can't do this in
write()and what it does instead. - Disambiguate streams. Write the declarations a scoreboard needs to tell an input monitor's stream from an output monitor's.
- Add a subscriber. Describe every change needed to add a coverage collector to an existing monitor — and confirm the monitor is untouched.
Summary
- A monitor delivers its observations through an analysis port — a one-to-many, non-blocking, broadcast connection (publish-subscribe):
ap.write(tr)fans each transaction out to every connected subscriber at once. - The broadcast is synchronous and zero-time: one
write()invokes all subscribers'write()methods in turn, in the same time-step, and the monitor neither blocks nor knows nor cares whether zero, one, or many are listening. - This decouples the monitor (producer) from the scoreboard, coverage, and loggers (consumers): subscribers are added or removed in
connect_phase— the monitor is never touched — and awrite()to a port with no subscribers is legal and harmless. - Because
write()is a zero-time function, a subscriber must not block inside it — time-consuming work is deferred (push to auvm_tlm_analysis_fifo, process inrun_phase) — and a subscriber listening to multiple streams needs a distinct analysis imp per source (uvm_analysis_imp_decl) because the broadcast is source-blind. - The durable rule of thumb: publish observations through a broadcast analysis port so the monitor stays decoupled from its consumers — add subscribers in
connect_phase, never in the monitor; keep everywrite()non-blocking (defer with a fifo); and give each incoming stream its own analysis imp so a multi-source subscriber can tell its sources apart.
Next — Monitor Debugging: the monitor observes, reconstructs, checks the protocol, and broadcasts — the next chapter closes the module with a debugging methodology for when the monitor itself is wrong: distinguishing a monitor bug (missed activity, mis-reconstruction, broadcast gaps) from a DUT bug, and the systematic checks that localize where the monitor's view of reality diverged from the pins.