Analysis ports: one call, many listeners
Publish one observed transaction to any number of listeners with an analysis port, receive it with imps, suffixed imps, subscribers and exports, and know why write is a function, in which order listeners hear, and why none of them may change what it received.
- Lesson
- U3.5
- Time
- 30 min
- Runs on
- Verilator with UVM
- Check
- 5 questions
A coverage collector masks the top bits of each packet's data before sampling, to fold 256 values into 16 bins. The scoreboard, which listens to the same monitor, starts reporting every packet as a mismatch. Both listeners received the same object, and the coverage collector changed it.
in_mon are calls of write, numbered in the order this library makes them. The FIFO's arrow to the checker is a returned handle.- Publish10 ns (illustrative)
At 10 ns
in_moncallsap.write(#1)once. The port callswriteon each of its three resolved imps, in turn, inside that one call.More context
Trace lines 1 to 3. The order is the imps' full names,
chk.fifobeforecovbeforesb, not the order of theconnectcalls. Every call carries the same handle.Follow the calls, values and waits
- call write(#1): in_mon → chk.fifo
- call write(#1): in_mon → cov
- call write(#1): in_mon → sb
- Return10 ns (illustrative)
Every
writeis a function, so all three finish at 10 ns andin_mongoes on at once. The FIFO stored the handle;covandsbdid their work.More context
Trace line 4. A function cannot wait, so no listener can hold the monitor up.
- Consume later10 ns (illustrative)
The checker's own thread takes #1 from the FIFO at 10 ns and works on it for 25 ns. The monitor never waited for it.
More context
Trace line 5. The checker's pending
fifo.getreturns in the same time step, but in the checker's thread, afterin_monhas gone on.Follow the calls, values and waits
- return #1: chk.fifo → chk run_phase
- Waiting: chk run_phase — working on #1 until 35 ns
- Second input15 ns (illustrative)
At 15 ns
out_monwrites the result to the scoreboard's second imp, which callswrite_out.More context
Trace lines 6 to 8. A second analysis port reaches a second imp of the same scoreboard;
sbmatches the observed #1 against the one it stored at 10 ns.Follow the calls, values and waits
- call write_out(#1): out_mon → sb
- Publish #2 into a busy checker20 ns (illustrative)
At 20 ns
in_monwrites #2. The FIFO stores it although the checker is still busy with #1.More context
Trace lines 9 to 12. The analysis FIFO is unbounded, so its
writealways succeeds at once;out_monthen writes #2's result at 25 ns.Follow the calls, values and waits
- call write(#2): in_mon → chk.fifo
- call write(#2): in_mon → cov
- call write(#2): in_mon → sb
- Take #235 ns (illustrative)
At 35 ns the checker finishes #1 and takes #2, which has waited in the FIFO since 20 ns.
More context
Trace lines 16 and 17. The FIFO absorbed the difference between the monitor's pace and the checker's.
Follow the calls, values and waits
- return #2: chk.fifo → chk run_phase
- Waiting: chk run_phase — working on #2 until 60 ns
- Released: chk run_phase
- Finish60 ns (illustrative)
The checker finishes #2 at 60 ns: it took 2 and finished 2.
More context
Trace line 18. The test waits for this count, not for the monitor, before it ends the run.
Follow the calls, values and waits
- Released: chk run_phase
Read the diagram as text
- in_mon: ap.write(t) at 10 and 20 ns
- chk.fifo: Analysis FIFO, unbounded
- cov: uvm_subscriber: write(t)
- sb: write_in(t) and write_out(r)
- chk run_phase: fifo.get(t), then 25 ns of work
- out_mon: ap.write(r) at 15 and 25 ns
- in_mon → chk.fifo: 1: write(t)
- in_mon → cov: 2: write(t)
- in_mon → sb: 3: write_in(t)
- chk.fifo → chk run_phase: t returns to get(t)
- out_mon → sb: write_out(r)
The publisher's code does not change with the number of listeners, zero included.
One call reaches every listener
An analysis port is a port whose interface has one method, write(T t), and which may reach any number of imps. A put port, with its default bounds, reaches exactly one. When you call ap.write(t), the port loops over its resolved imps and calls write(t) on each, then returns.
Zero listeners is legal. The example's spare_ap has none: size() is 0 and write returns without a message. That is why a monitor can always publish, whether or not this bench uses its output.
| Analysis port | Put port | |
|---|---|---|
| Method | write(t), a function | put(t), a task |
| Imps it calls | Every resolved imp, 0 or more | The one selected imp; 1 by default |
| Can the receiver make the caller wait? | No | Yes, as long as it likes |
| Typical caller | A monitor publishing what it saw | A generator handing work to one consumer |
| Receiver that needs time | Puts it in an analysis FIFO and works in its own thread | Simply takes the time |
Analysis ports report what happened. Put ports ask someone to do something.
Why write is a function
A monitor samples the bus at every clock edge. If a listener could wait inside write, the monitor would be stuck in that call and miss the next transfer. Making write a function rules this out at compile time: #5 in a write gives Functions cannot contain time-controlling statements.
A listener that needs time therefore splits its work. Its write stores the packet, and a task in its own thread does the slow part. The checker environment does exactly that with a uvm_tlm_analysis_fifo, which never fills, so write always succeeds.
in_mon: ap.write(#1) at 10 ns
Inside this call: the FIFO stores the handle,
cov.writesamples,sb.write_inqueues the expected packet.in_mon: write returned at 10 ns
Zero time has passed. The monitor is free for the next edge.
chk: fifo.get -> #1 at 10 ns
The checker's thread was waiting in
get. It takes #1 and works for 25 ns.chk: fifo.get -> #2 at 35 ns
#2 was published at 20 ns and waited in the FIFO for 15 ns. Nobody upstream noticed.
chk: finished #2 at 60 ns
The monitor dropped its objection at 50 ns, while #2 was still in work. The test waits for the checker to report both checks done, against a 200 ns deadline. Here the checks win at 60 ns and the run ends cleanly. Had the deadline won, the test would report a
UVM_ERRORnaming the taken and finished counts. Countinggets alone would have claimed two checks at 35 ns.
Who hears first
The test connects the scoreboard first, then coverage, then the checker. The trace shows a different order: FIFO, cov, sb. This library keeps a port's resolved imps in an associative array indexed by their full names, so delivery follows name order: uvm_test_top.chk.fifo... sorts before uvm_test_top.cov..., which sorts before uvm_test_top.sb....
That order is a detail of this implementation, not a promise of the standard. Rename cov to zcov and it hears last. A design whose result depends on which listener runs first is broken, even if it passes today.
The same holds between listeners and the publisher's next step. Every write finishes before ap.write returns, so a listener cannot see the monitor's next packet early. In this run, the checker woken through the FIFO ran only after the monitor's thread reached its next wait: trace line 5 comes after line 4.
Receiving: imps, suffixes, subscribers and exports
A listener receives through uvm_analysis_imp #(T, IMP), which calls write on its owner. One component with two analysis inputs has a problem: both imps would call the same write.
uvm_analysis_imp_decl(_in) solves it. The macro declares a new imp class, uvm_analysis_imp_in, whose write calls write_in on the owner. The scoreboard declares _in and _out and gets two imps and two methods.
| Receiver | Use it when | What it provides |
|---|---|---|
uvm_analysis_imp #(T, IMP) | One input, any base class | Calls IMP::write(t) |
uvm_analysis_imp_decl(_sfx) | Two or more inputs on one component | A class uvm_analysis_imp_sfx calling write_sfx(t) |
uvm_subscriber #(T) | A component with exactly one input | analysis_export (an imp) and a pure virtual write |
uvm_analysis_export #(T) | A parent exposing an input of its child | Forwards to the child's imp; must reach at least one |
An export is a forwarder, so it must lead to an imp. An unconnected uvm_analysis_export is a build error.
Publishing through a parent works the other way round. An agent usually offers its monitor's output as its own ap. Either connect mon.ap.connect(ap) in the agent, a port-to-port link as in U3.2, or copy the handle, ap = mon.ap;, in connect_phase. With the copy, both names refer to one port object, so listeners connected to agent.ap are listeners of the monitor's port.
The checker environment shows the last row. Its analysis_export is the public input, connected inside to fifo.analysis_export. The test connects to the environment's edge and never names the FIFO. An analysis export with nothing behind it reports connection count of 0 does not meet required minimum of 1 and stops the build.
Every listener gets the same object
ap.write(#1) returned and before the checker's thread runs. The coverage collector kept no handle.- Change
Had the coverage collector written
t.data = t.data & 8'h0fduring itswrite, this one object, which all three still refer to, would already hold 01.More context
Nothing moves here: the arrows are references, and changing the object changes what all three of them lead to.
- Mismatch15 ns (illustrative)
At 15 ns the scoreboard compares its stored packet, now
data=01, with the resulta1and reports an error.More context
The scoreboard reads
datathrough its handle at compare time, so it gets the masked value. In the masking variant,sb.write_inalready printsdata=01at 10 ns.Follow the calls, values and waits
- data data = 01: pkt #1 → sb.expected[0]
Read the diagram as text
- in_mon local t: The publisher still holds the packet it wrote
- sb.expected[0]: Stored by write_in for the later compare
- chk.fifo entry: Queued; the checker takes it later in the same time step
- pkt #1: data = a1; becomes 01 if cov masks in place
- in_mon local t → pkt #1: handle
- sb.expected[0] → pkt #1: handle
- chk.fifo entry → pkt #1: handle
Nothing was copied by the analysis port. Copy before changing, or do not change.
write passes a handle. All three listeners receive the same pkt object, and the scoreboard stores that handle as its expected packet. Change the coverage collector to mask the data in place, t.data = t.data & 8'h0f, and the run shows sb.write_in: #1 data=01.
Order does not save you. If the scoreboard heard first, it would store the handle, and the coverage collector would change the stored packet a moment later. A listener that needs a different value must make its own copy. U7.2 shows the other half of the rule: a monitor must publish a new object each time.
Try it
The first line lists the resolved imps in name order, the order the trace then follows. Note the subscriber's imp: uvm_subscriber names its analysis_export member analysis_imp.
The checker's two lines took and finished count different things: a packet taken from the FIFO is still in work for 25 ns. On success the test ends the run when finished reaches 2, at 60 ns, ten nanoseconds after the monitor dropped its objection. If the 200 ns deadline came first, it would raise CHK as a UVM_ERROR with both counts, so unfinished work fails the run instead of ending it quietly (U7.3 makes the same split between queued, in-flight and checked work).
Try it: replace the body of cov.write with t.data = t.data & 8'h0f; and rerun. The scoreboard stores the changed packet and both results mismatch. Then rename the coverage instance to zcov: the order changes, and the mismatches stay.
im.ap reaches 3 imps, in this order: uvm_test_top.chk.fifo.analysis_export uvm_test_top.cov.analysis_imp uvm_test_top.sb.in_imp spare_ap: size()=0, a write with no listeners returned 1 10 ns in_mon: ap.write(#1 data=a1) 2 10 ns cov.write: #1 data=a1 3 10 ns sb.write_in: #1 data=a1, expected at the output 4 10 ns in_mon: write returned 5 10 ns chk: fifo.get -> #1 data=a1 6 15 ns out_mon: ap.write(#1 data=a1) 7 15 ns sb.write_out: #1 data=a1 matches 8 15 ns out_mon: write returned 9 20 ns in_mon: ap.write(#2 data=a2) 10 20 ns cov.write: #2 data=a2 11 20 ns sb.write_in: #2 data=a2, expected at the output 12 20 ns in_mon: write returned 13 25 ns out_mon: ap.write(#2 data=a2) 14 25 ns sb.write_out: #2 data=a2 matches 15 25 ns out_mon: write returned 16 35 ns chk: finished #1 17 35 ns chk: fifo.get -> #2 data=a2 18 60 ns chk: finished #2 cov sampled 2; sb matched 2, mismatched 0; chk took 2, finished 2 UVM_ERROR : 0Check yourself
Remember
- An analysis port's
writecalls every resolved imp in turn, inside one call; zero listeners is legal. writeis a function, so no listener can delay the publisher; a listener that needs time stores the item in an analysis FIFO and works in its own thread.- This library delivers in the order of the imps' full names, not connect order; never depend on it.
- Use
uvm_analysis_imp,uvm_analysis_imp_declfor several inputs,uvm_subscriberfor one, anduvm_analysis_exportto expose a child's input. - Every listener receives the same object; a listener that changes it changes it for all.