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.

One write, three listenersArrows leaving 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.
call: who calls whomdata: where the transaction goes
One write, three listenersArrows leaving `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. publisher in_mon ap.write(t) at 10 and20 ns queue chk.fifo Analysis FIFO,unbounded subscriber cov uvm_subscriber:write(t) subscriber sb write_in(t) andwrite_out(r) component chk run_phase fifo.get(t), then 25ns of work publisher out_mon ap.write(r) at 15 and25 ns 1: write(t)2: write(t)3: write_in(t)t returns to get(t)write_out(r)
  1. Publish10 ns (illustrative)

    At 10 ns in_mon calls ap.write(#1) once. The port calls write on 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.fifo before cov before sb, not the order of the connect calls. Every call carries the same handle.

    Follow the calls, values and waits
    1. call write(#1): in_mon → chk.fifo
    2. call write(#1): in_mon → cov
    3. call write(#1): in_mon → sb
  2. Return10 ns (illustrative)

    Every write is a function, so all three finish at 10 ns and in_mon goes on at once. The FIFO stored the handle; cov and sb did their work.

    More context

    Trace line 4. A function cannot wait, so no listener can hold the monitor up.

  3. 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.get returns in the same time step, but in the checker's thread, after in_mon has gone on.

    Follow the calls, values and waits
    1. return #1: chk.fifo → chk run_phase
    2. Waiting: chk run_phase — working on #1 until 35 ns
  4. Second input15 ns (illustrative)

    At 15 ns out_mon writes the result to the scoreboard's second imp, which calls write_out.

    More context

    Trace lines 6 to 8. A second analysis port reaches a second imp of the same scoreboard; sb matches the observed #1 against the one it stored at 10 ns.

    Follow the calls, values and waits
    1. call write_out(#1): out_mon → sb
  5. Publish #2 into a busy checker20 ns (illustrative)

    At 20 ns in_mon writes #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 write always succeeds at once; out_mon then writes #2's result at 25 ns.

    Follow the calls, values and waits
    1. call write(#2): in_mon → chk.fifo
    2. call write(#2): in_mon → cov
    3. call write(#2): in_mon → sb
  6. 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
    1. return #2: chk.fifo → chk run_phase
    2. Waiting: chk run_phase — working on #2 until 60 ns
    3. Released: chk run_phase
  7. 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
    1. 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 portPut port
Methodwrite(t), a functionput(t), a task
Imps it callsEvery resolved imp, 0 or moreThe one selected imp; 1 by default
Can the receiver make the caller wait?NoYes, as long as it likes
Typical callerA monitor publishing what it sawA generator handing work to one consumer
Receiver that needs timePuts it in an analysis FIFO and works in its own threadSimply 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.

  1. in_mon: ap.write(#1) at 10 ns

    Inside this call: the FIFO stores the handle, cov.write samples, sb.write_in queues the expected packet.

  2. in_mon: write returned at 10 ns

    Zero time has passed. The monitor is free for the next edge.

  3. chk: fifo.get -> #1 at 10 ns

    The checker's thread was waiting in get. It takes #1 and works for 25 ns.

  4. chk: fifo.get -> #2 at 35 ns

    #2 was published at 20 ns and waited in the FIFO for 15 ns. Nobody upstream noticed.

  5. 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_ERROR naming the taken and finished counts. Counting gets 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.

ReceiverUse it whenWhat it provides
uvm_analysis_imp #(T, IMP)One input, any base classCalls IMP::write(t)
uvm_analysis_imp_decl(_sfx)Two or more inputs on one componentA class uvm_analysis_imp_sfx calling write_sfx(t)
uvm_subscriber #(T)A component with exactly one inputanalysis_export (an imp) and a pure virtual write
uvm_analysis_export #(T)A parent exposing an input of its childForwards 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

Three references, one packetArrows are references, not calls or copies. This is the state at 10 ns, just after ap.write(#1) returned and before the checker's thread runs. The coverage collector kept no handle.
Three references, one packetArrows are references, not calls or copies. This is the state at 10 ns, just after `ap.write(#1)` returned and before the checker's thread runs. The coverage collector kept no handle. reference in_mon local t The publisher stillholds the packet itwrote reference sb.expected[0] Stored by write_infor the later compare reference chk.fifo entry Queued; the checkertakes it later in thesame time step shared object pkt #1 data = a1; becomes 01if cov masks in place handlehandlehandle
  1. Change

    Had the coverage collector written t.data = t.data & 8'h0f during its write, 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.

  2. Mismatch15 ns (illustrative)

    At 15 ns the scoreboard compares its stored packet, now data=01, with the result a1 and reports an error.

    More context

    The scoreboard reads data through its handle at compare time, so it gets the masked value. In the masking variant, sb.write_in already prints data=01 at 10 ns.

    Follow the calls, values and waits
    1. 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.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: 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 : 0

Check yourself

Predict the outputAn analysis port is connected to three subscribers named zeta, alpha and mid, in that order. Each subscriber's write appends its name and a space to a global string. After one write, what does the test print?
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;

string heard;

class listener extends uvm_subscriber #(int);
  `uvm_component_utils(listener)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void write(int t); heard = {heard, get_name(), " "}; endfunction
endclass

class order_test extends uvm_test;
  `uvm_component_utils(order_test)
  uvm_analysis_port #(int) ap;
  listener zeta, alpha, mid;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    ap    = new("ap", this);
    zeta  = listener::type_id::create("zeta", this);
    alpha = listener::type_id::create("alpha", this);
    mid   = listener::type_id::create("mid", this);
  endfunction
  function void connect_phase(uvm_phase phase);
    ap.connect(zeta.analysis_export);
    ap.connect(alpha.analysis_export);
    ap.connect(mid.analysis_export);
  endfunction
  task run_phase(uvm_phase phase);
    phase.raise_objection(this);
    ap.write(1);
    $display("%s", heard);
    phase.drop_objection(this);
  endtask
endclass

module tb;
  initial run_test("order_test");
endmodule
Spot the bugEach option is the first line of the coverage collector's write(pkt t); hits is an int associative array and seen a queue of pkt. The scoreboard, which hears after cov, stores each handle and compares it with the result. Which option makes the scoreboard report mismatches?
Spot the bugEach option is the checker's work time per packet, taken from its analysis FIFO. Packets are published at 10 and 20 ns. The test waits for two finished checks against a 200 ns deadline and raises a UVM_ERROR if the deadline wins. Which work time makes the run fail?
ConceptWhy does UVM make write a function instead of a task?
ConceptA scoreboard must receive packets from an input monitor and an output monitor. Which receiving side works?

Remember

  • An analysis port's write calls every resolved imp in turn, inside one call; zero listeners is legal.
  • write is 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_decl for several inputs, uvm_subscriber for one, and uvm_analysis_export to expose a child's input.
  • Every listener receives the same object; a listener that changes it changes it for all.