Part 4 · TLM & Analysis · Intermediate
analysis_port Broadcast: write() Fan-Out Without Backpressure
How uvm_analysis_port.write() delivers to all connected imps/subscribers, delivery ordering implications, and why the path must stay non-blocking.
write() means publish to everyone connected
A uvm_analysis_port#(T) is a publisher endpoint. When the producer calls write(tr), UVM dispatches that transaction to every connected analysis implementation endpoint.
This is fan-out, not selection. If three consumers are connected, each receives the callback. If none are connected, the call returns and nothing observes the sample.
class apb_monitor extends uvm_monitor;
`uvm_component_utils(apb_monitor)
uvm_analysis_port #(apb_txn) ap;
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
task run_phase(uvm_phase phase);
apb_txn tr;
forever begin
tr = sample_apb();
ap.write(tr); // broadcast to all connected consumers
end
endtask
endclass[MON][TLM] fan-out picture
tr0 tr1 tr2 ...
[MON] ap.write(tr) each sample
│
├─► sb.write(tr)
├─► cov.write(tr)
└─► pred.write(tr)
same producer call fans to all connected endpointsProducer writes once; N subscribers receive.
Connectivity defines recipients, not producer-side branch logic.
Broadcast path keeps monitor reusable and checker composition flexible.
No backpressure is a design feature
Analysis write() is intended to stay non-blocking from the producer perspective. Monitors should not stop observing traffic because a checker is busy.
If a consumer does expensive work or blocks, it can degrade simulation and distort monitor behavior. Use lightweight write() handlers, and offload heavy work to queues/FIFOs when needed.
[TLM] no-backpressure consequence
producer perspective:
monitor keeps sampling bus
monitor calls ap.write(tr)
monitor does not negotiate ready/valid with subscribers
consumer responsibility:
process quickly in write()
or enqueue + return
do heavy compare in another threadclass safe_scoreboard extends uvm_component;
`uvm_component_utils(safe_scoreboard)
uvm_analysis_imp #(apb_txn, safe_scoreboard) in;
apb_txn q[$];
function new(string name, uvm_component parent);
super.new(name, parent);
in = new("in", this);
endfunction
function void write(apb_txn tr);
q.push_back(tr.clone()); // quick ingestion
endfunction
task run_phase(uvm_phase phase);
forever begin
wait (q.size() > 0);
compare_one(q.pop_front()); // heavier work outside write()
end
endtask
endclass[CHECK] ingestion strategy
write() path: fast copy + queue
processing path: separate thread
benefit: monitor timeline remains stable
risk if ignored: long write() stalls global simulation progressKeep write() short and deterministic.
Queue-then-process pattern protects monitor observability fidelity.
Use analysis_fifo when decoupling/ordering guarantees are needed.
Delivery ordering and transaction identity
Within a producer stream, call order is the natural sequence seen by subscribers. However, each subscriber can process at different speeds after ingestion, so downstream report timing may differ.
Because all consumers may receive the same object handle, mutation discipline matters . If one subscriber edits fields in-place, other consumers may observe changed data unexpectedly.
[MON][CHECK] shared handle risk
monitor creates tr
├─► subscriber A writes: tr.kind = ERR (mutates)
├─► subscriber B sees kind=ERR (unexpected for raw monitor view)
└─► subscriber C logs modified form
fix:
clone on ingestion before local mutationfunction void write(apb_txn tr);
apb_txn local;
local = tr.clone();
local.set_id_info(tr);
// safe local mutation for checker-specific normalization
local.addr[1:0] = 2'b00;
normalized_q.push_back(local);
endfunction[TLM] mutation policy options
Option A: monitor sends immutable objects (team rule)
Option B: every subscriber clones before mutation
Option C: dedicated normalizer subscriber outputs new stream
choose one policy and enforce consistentlyDo not assume each subscriber receives independent transaction objects.
Clone before mutation unless immutability is guaranteed by policy.
Sequence order at producer is stable; subscriber completion timing is independent.
Walkthrough: monitor broadcasting to scoreboard and coverage
This walkthrough shows the canonical pattern: monitor writes one transaction stream, scoreboard checks correctness, and coverage bins protocol scenarios from the same passive observations.
class env extends uvm_env;
`uvm_component_utils(env)
apb_agent agt;
apb_scoreboard sb;
apb_cov_sub cov;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
agt = apb_agent::type_id::create("agt", this);
sb = apb_scoreboard::type_id::create("sb", this);
cov = apb_cov_sub::type_id::create("cov", this);
endfunction
function void connect_phase(uvm_phase phase);
agt.mon.ap.connect(sb.in);
agt.mon.ap.connect(cov.analysis_export);
endfunction
endclass[MON][TLM][CHECK] end-to-end timeline
t0 monitor samples bus -> tr0
t1 monitor.ap.write(tr0)
t2 scoreboard ingest tr0
t3 coverage ingest tr0
t4 monitor samples bus -> tr1
...
one passive source, multiple analysis consumersKey takeaways
analysis_port.write() is broadcast fan-out and should be treated as non-blocking ingestion.
Keep heavy checker logic out of write() to avoid simulation distortion.
Define clear transaction mutation/clone policy across subscribers.
Monitor broadcast enables consistent checking and coverage from one observation stream.
Common pitfalls
Long-running write() functions that hide performance and timing issues.
Subscriber-side mutation of shared objects without clone protection.
Assuming analysis broadcast provides ready/ack flow control semantics.
Connecting only one checker and forgetting other intended consumers.