Skip to content

UVM

Data Flow Through UVM

How transactions move via TLM ports, exports, and imps — the blocking sequencer-driver pull and the non-blocking analysis broadcast — as a graph separate from the hierarchy.

UVM Testbench Architecture · Module 3 · Page 3.4

The Engineering Problem

You built the static tree (Module 3.3) and you know what flows through it (transactions). Now the runtime question: how does a transaction actually get from one component to another? And here is the fact that trips up everyone: the data-flow graph is not the containment tree. The monitor lives inside the agent, but it sends transactions to the scoreboard, which lives in the environment — a path that runs sideways across the hierarchy, not up or down it. Components don't pass data by parent-child relationship; they pass it through explicit TLM connections wired separately.

So a UVM environment has two graphs laid over the same components: the containment hierarchy (who builds and owns whom — Module 3.3) and the data-flow graph (who sends transactions to whom — this chapter). Get the first wrong and components aren't phased; get the second wrong and they're phased but starved — a driver that blocks forever waiting for a transaction, or a scoreboard that silently receives nothing. The connections that carry data are their own structure, with their own rules, and you must build them deliberately.

How does a transaction move from component to component at run time — through which TLM connections — and why is that data-flow graph separate from the containment hierarchy?

Motivation — why data flow is its own structure

Treating the data-flow graph as a first-class thing, distinct from the hierarchy, is what makes a runnable, debuggable environment:

  • Data flows sideways, not along ownership. The monitor (in the agent) feeds the scoreboard (in the env); the sequencer (in the agent) feeds the driver (also in the agent, but the connection is still explicit). None of these follow parent-child links. You must wire the data path, and knowing it is separate from containment is what lets you wire it correctly.
  • The connection style encodes the semantics. A blocking pull (get_next_item) means the driver waits for the sequencer — flow control built in. A non-blocking broadcast (write) means the monitor fires and forgets — decoupled, one-to-many. Choosing the right TLM pattern is choosing the right runtime behaviour.
  • Most runtime hangs and silent passes are connection bugs. A driver stuck forever is almost always an unconnected seq_item_port; a scoreboard that checks nothing is almost always an unconnected analysis_port. These are data-flow-graph failures, invisible in the (correct) containment tree.
  • It is the basis of decoupling and reuse. Because producers connect to ports rather than calling specific consumers, you can re-wire the data flow — add a coverage subscriber, swap a scoreboard — without changing the components. The connection graph is where the environment's flexibility lives.

The motivation, in one line: the data-flow graph is a separate structure with its own connections and semantics, and almost every "it built fine but doesn't run right" bug is a wire missing from that graph.

Mental Model

Hold two overlays on the same components:

The hierarchy is the org chart; the data flow is the plumbing — and the plumbing doesn't follow the org chart. The org chart (containment) says who owns whom: the agent owns the monitor, the env owns the scoreboard. The plumbing (TLM connections) says where water flows: from the monitor, across to the scoreboard, regardless of who owns either. You lay the plumbing explicitly in connect_phase by joining a port (an outlet — the initiator that starts a transfer) to an export or imp (an inlet — the provider that receives or implements it). Two kinds of plumbing exist: a demand pipe where the consumer pulls and the producer must supply on demand (sequencer→driver, blocking), and a broadcast spray where one source sprays to any number of catchers (monitor→subscribers, non-blocking). A component built correctly (in the org chart) but with no plumbing connected is a room with power but no water — present, phased, and useless.

So for any transaction, ask two separate questions: who owns this component (hierarchy)? and where does its data go (which port connects to which export)? The answers are independent, and the second is the one that makes data actually move.

Visual Explanation — two graphs on the same components

The single most important picture is the overlay: the same components carry a containment tree and a data-flow graph, and they don't match.

Containment edges (env to agent, agent to sequencer/driver/monitor) versus data-flow edges (sequencer to driver, monitor to scoreboard and coverage) over the same componentsownsownsownsownsownspull (data)broadcast (data)envowns agent + scoreboardagentowns sqr/drv/monsequencerdriverpulls from sequencermonitorbroadcastsscoreboardin env, fed by monitor12
Figure 1 — two graphs over the same components. Containment (solid, vertical) is who owns whom: env owns the agent and scoreboard; the agent owns the sequencer, driver, and monitor. Data flow (dashed) is who sends transactions to whom: the sequencer feeds the driver (a blocking pull), and the monitor feeds the scoreboard and coverage (a non-blocking broadcast) — a path that runs across the hierarchy, not along it. The monitor lives in the agent but sends to the scoreboard in the env; data flow ignores ownership.

The two edge sets are the whole lesson. The solid containment edges go straight down the tree (Module 3.3's structure). The dashed data-flow edges cut sideways: sequencer → driver and monitor → scoreboard cross between siblings and across hierarchy levels. The monitor sends to the scoreboard not because of any ownership relationship — they are in different subtrees — but because you explicitly connected its analysis port to the scoreboard's export. Data flow is wired, not inherited, and recognising it as a separate graph is what lets you reason about (and debug) where transactions actually go.

RTL / Simulation Perspective — wiring the data-flow graph

The data-flow graph is built in connect_phase with connect() calls that join ports to exports/imps. Below, the monitor declares a broadcast port, the driver pulls from the sequencer, and the env wires both connections.

TLM connections — the data-flow graph, wired in connect_phase
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class my_monitor extends uvm_monitor;
  `uvm_component_utils(my_monitor)
  uvm_analysis_port#(bus_txn) ap;            // broadcast port: non-blocking, one-to-many
  function void build_phase(uvm_phase phase); ap = new("ap", this); endfunction
  task run_phase(uvm_phase phase);
    bus_txn t;
    forever begin
      collect(t);                            // reconstruct a transaction from the pins
      ap.write(t);                           // BROADCAST it — non-blocking, to any subscribers
    end
  endtask
endclass
 
class my_env extends uvm_env;
  `uvm_component_utils(my_env)
  my_agent agent;  my_scoreboard sb;  my_cov cov;
  // ... build_phase creates agent, sb, cov ...
 
  function void connect_phase(uvm_phase phase);
    // BLOCKING PULL: driver gets items from the sequencer (port → export)
    agent.drv.seq_item_port.connect(agent.sqr.seq_item_export);
    // NON-BLOCKING BROADCAST: monitor feeds scoreboard AND coverage (one-to-many)
    agent.mon.ap.connect(sb.analysis_export);
    agent.mon.ap.connect(cov.analysis_export);
  endfunction
endclass

Read the two patterns. The blocking pull: the driver's seq_item_port.connect(sqr.seq_item_export) joins the driver (initiator) to the sequencer (provider); at run time the driver calls get_next_item, which blocks until the sequencer supplies a transaction, then item_done after driving — built-in flow control. The non-blocking broadcast: the monitor's ap is an analysis_port, connected to both the scoreboard's and coverage's analysis exports; ap.write(t) sends the transaction to every connected subscriber and returns immediately. Notice the direction convention — the port (initiator/producer side) calls connect on the export (provider/consumer side) — and that both connect calls live in connect_phase, after build_phase has created the components they join.

Verification Perspective — ports, exports, and imps

The TLM connection mechanism has three roles, and knowing which is which is what lets you wire connections in the right direction. A port initiates; an imp implements; an export forwards between them.

TLM roles: port initiates a transfer, export forwards across a hierarchy boundary, imp implements the methodPort initiates, export forwards, imp implementsPort initiates, export forwards, imp implements1Port — the initiatorthe component that starts the transfer: driver's seq_item_port(pull), monitor's analysis_port (broadcast).2Export — the forwarderpasses a connection from a nested implementer out to a component'sboundary, so it can be connected from outside.3Imp — the implementerimplements the actual method invoked: the subscriber's write(), orthe sequencer providing items.4Connect port → export/impthe port calls connect() on the export/imp; at run time the port'scall reaches the imp's method. Data flows initiator → implementer.
Figure 2 — the TLM port/export/imp model. A port is the initiator — the component that starts a transfer (the driver's seq_item_port, the monitor's analysis_port). An imp implements the actual method that does the work (the consumer's write(), or the sequencer's item-provision). An export forwards a connection from a child up to a parent's boundary when the implementer is nested. You connect a port to an export or an imp; the port calls, the imp implements, and data flows from initiator to implementer.

The three roles answer "which way does connect go?" The port is held by the initiator — the side that calls a method (the driver pulling, the monitor broadcasting). The imp is held by the implementer — the side that provides the method body (the write() in a uvm_subscriber, the item provision in a sequencer). The export is plumbing for when the implementer is nested inside a component and its capability must be exposed at the component's boundary. You always connect from the initiator's port toward the implementer's export/imp, and at run time the port's call (get_next_item, write) is routed to the imp's implementation. Getting the direction right is just asking who calls (port) and who implements (imp) — and connecting the former to the latter.

Runtime / Execution Flow — the full path of a transaction

Put the connections together and a transaction's complete journey is a chain of TLM transfers and one pin-level translation, end to end from the sequence that creates it to the scoreboard that checks it.

Full data path: sequence to sequencer to driver (blocking pull), driver to DUT to monitor (pins), monitor to scoreboard and coverage (analysis broadcast)transactionpull:get_next_itemdrive (pins)collect(pins)write (broadcast)write (broadcast)Sequencecreates txnSequencerprovides on demandDriverblocking pullDUTMonitorbroadcastsScoreboardchecksCoveragesamples12
Figure 3 — the full data path. The sequence creates a transaction and sends it to the sequencer; the driver pulls it via the blocking seq_item connection (get_next_item) and drives the DUT; the monitor reconstructs it from the pins and broadcasts it via the non-blocking analysis connection to the scoreboard (which checks) and coverage (which samples). Every hop except the pins is a TLM transfer; the two pin-level translations (drive, collect) are the only signal-level steps. This chain is the data-flow graph in motion.

This chain is the runtime life of the data-flow graph. The first hop (sequence → sequencer → driver) is a blocking pull: the driver asks and waits, so stimulus is paced by the driver's readiness. The middle (driver → DUT → monitor) is the only signal-level part — the two translations you met in Module 3.2. The last hop (monitor → scoreboard/coverage) is a non-blocking broadcast: the monitor fires the transaction to all subscribers and moves on. Each hop is a TLM connection you wired in connect_phase, and the transaction is the payload moving along them. Trace any "where did this transaction go?" question along this chain, and trace any "why didn't it arrive?" question to the connection on the hop that's missing.

Waveform Perspective — blocking pull vs non-blocking broadcast

The two TLM patterns differ in time, and the waveform shows it: the sequencer→driver pull is a paced handshake (the driver waits for each item), while the monitor→subscriber broadcast is an instantaneous fire-and-forget after each observation.

The two TLM patterns in time — a paced pull and an instantaneous broadcast

10 cycles
The two TLM patterns in time — a paced pull and an instantaneous broadcastBlocking pull: driver's get_next_item receives a transaction (seq_item_port → export)Blocking pull: driver'…Non-blocking broadcast: monitor's analysis_port.write() fans out to subscribersNon-blocking broadcast…pull (sequencer → driver) and broadcast (monitor → scoreboard/coverage) carry all data flowpull (sequencer → driv…clkseq_pullvaliddata00A0A100B0B100000000ana_bcastt0t1t2t3t4t5t6t7t8t9
Figure 4 — the driver pulls a transaction from the sequencer (seq_pull) and drives it onto the pins; this is blocking, so the driver waits for each item before driving. After observing the completed transfer, the monitor broadcasts it (ana_bcast) to the scoreboard and coverage; this is non-blocking and returns immediately. The pull paces stimulus (sequencer → driver); the broadcast fans out observation (monitor → subscribers). Together these two patterns carry all data flow in a UVM environment.

The seq_pull and ana_bcast pulses are the two halves of the data-flow graph at work. The pull is paced: the driver requests an item, the sequencer provides it, and only then does the driver drive — so the stimulus rate is governed by the driver/handshake, with flow control inherent in the blocking call. The broadcast is immediate: the moment the monitor finishes reconstructing a transaction, write() delivers it to every subscriber and returns, with no waiting and no knowledge of who listens. These two timing behaviours — demand-paced pull in, fire-and-forget broadcast out — are why the sequencer→driver and monitor→subscriber connections use different TLM mechanisms, and together they move every transaction through the environment.

DebugLab — the driver that blocked forever

The run hung with nothing driven — the sequencer-driver connection was never made

Symptom

A testbench built correctly, the test started a sequence, and then the simulation hung — it ran but never progressed, no transactions reached the DUT, and eventually it timed out. The driver, the sequencer, and the sequence all existed in the tree at the right paths (topology was clean), yet not one transaction was ever driven. The driver appeared stuck.

Root cause

A missing data-flow connection: the driver's seq_item_port was never connected to the sequencer's seq_item_export, so the driver's get_next_item() blocked forever waiting for a transaction that could never arrive:

why the driver blocked forever
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
build_phase:   sequencer, driver created (both in the tree, correct paths)   ✓
connect_phase: drv.seq_item_port.connect(sqr.seq_item_export);   ← MISSING
driver run:    seq_item_port.get_next_item(req);   // blocks until an item arrives...
sequencer:     has items, but no connected port to deliver them to → never reaches driver
result:        get_next_item() blocks forever → driver hangs → no stimulus → timeout
fix:           add the connect() in connect_phase (port → export, correct direction)

The containment tree was perfect — every component built and phased — but the data-flow edge from sequencer to driver was never wired, so the blocking pull had nothing to pull from. A correct hierarchy with a missing connection.

Diagnosis

The tell is a driver stuck in get_next_item with a clean topology — a data-flow gap, not a hierarchy gap. Diagnose connection problems separately from construction:

  1. A hung driver means an unconnected (or misconnected) seq_item_port. get_next_item blocks by design; if it never returns, the port isn't connected to a sequencer, or is connected in the wrong direction. Check connect_phase for the connect call.
  2. Verify the direction. The driver's port connects to the sequencer's export (drv.seq_item_port.connect(sqr.seq_item_export)), not the reverse. Initiator's port → provider's export.
  3. Separate the two graphs. A clean print_topology() (containment correct) plus no data movement points squarely at the data-flow graph — inspect the connect() calls, not the tree.
Prevention

The data-flow graph must be wired deliberately and completely:

  1. Connect every port in connect_phase. Each seq_item_port to a sequencer's export, each analysis_port to its subscribers. A port with no connection is a hang (blocking pull) or a silent no-op (analysis broadcast).
  2. Wire in the correct direction. Initiator's port → implementer's export/imp. Reversed connections fail to compile or don't carry data; get the port/imp roles straight (who calls vs who implements).
  3. Verify data actually moves. Don't trust a clean topology — confirm the driver drives and the scoreboard receives (count transactions). A built-but-unconnected environment looks healthy in the tree and does nothing at run time.

The one-sentence lesson: a clean hierarchy does not mean a connected data path — a driver that blocks forever in get_next_item has an unwired seq_item_port, so check the connect() calls in connect_phase, not the component tree.

Common Mistakes

  • Confusing the data-flow graph with the containment tree. Data flows through explicit TLM connections, not parent-child links — the monitor sends to the scoreboard across the hierarchy. Wire data flow deliberately in connect_phase; don't assume containment carries it.
  • Forgetting to connect a port. An unconnected seq_item_port makes the driver block forever (hang); an unconnected analysis_port makes write() a silent no-op (scoreboard receives nothing, false pass). Connect every port.
  • Wrong connection direction. Connect the initiator's port to the implementer's export/imp (port.connect(export)), not the reverse. Mixing up who-calls (port) and who-implements (imp) gives a connection that doesn't carry data.
  • Connecting outside connect_phase. Connections join components that build_phase created, so they belong in connect_phase — after build, before run. Wiring earlier (components don't exist yet) or later (run already started) breaks the flow.
  • Assuming a clean topology means a working environment. print_topology() shows containment, not connections. A perfectly-built tree with missing TLM wires builds fine and does nothing — verify data actually moves.
  • Using a blocking pattern where a broadcast is needed (or vice versa). Sequencer→driver is a blocking pull (flow-controlled); monitor→subscribers is a non-blocking broadcast (one-to-many, fire-and-forget). Using the wrong one breaks pacing or fan-out.

Senior Design Review Notes

Interview Insights

Through explicit TLM (transaction-level modelling) connections, which form a data-flow graph that is separate from the containment hierarchy. Components are wired in connect_phase by joining ports to exports or imps, and two patterns carry essentially all data flow. The first is the blocking pull from sequencer to driver: the driver's seq_item_port connects to the sequencer's seq_item_export, and at run time the driver calls get_next_item, which blocks until the sequencer provides a transaction, then item_done after driving — this gives built-in flow control. The second is the non-blocking analysis broadcast from monitor to subscribers: the monitor's analysis_port connects to one or more subscribers' analysis exports, and write() delivers the transaction to all of them and returns immediately — one-to-many and decoupled. The full path of a transaction is sequence → sequencer → driver → DUT → monitor → scoreboard/coverage, and crucially that path runs across the hierarchy (the monitor in the agent feeds the scoreboard in the env), not along parent-child links.

Exercises

  1. Two graphs. For a standard agent + env, draw the containment edges and the data-flow edges separately, and identify one data-flow edge that crosses the hierarchy (connects components in different subtrees).
  2. Pick the pattern. For each connection, say whether it's a blocking pull or a non-blocking broadcast and why: (a) sequencer to driver; (b) monitor to scoreboard; (c) monitor to coverage; (d) monitor to a second, later-added logger.
  3. Diagnose two failures. Distinguish the cause of (a) a driver that hangs in get_next_item and (b) a scoreboard that receives zero transactions while the sim runs to completion. Name the missing connection in each.
  4. Direction. Write the connect() call to wire a driver to a sequencer and a monitor to a scoreboard, and explain, using the port/imp roles, why each goes in the direction it does.

Summary

  • Transactions move through explicit TLM connections — a data-flow graph that is separate from the containment hierarchy. The monitor (in the agent) feeds the scoreboard (in the env): data flow runs across the tree, not along ownership.
  • The connection roles are port (initiator — who calls), imp (implementer — who provides the method), and export (forwarder across a boundary). You wire them in connect_phase, connecting the initiator's port to the implementer's export/imp.
  • Two patterns carry everything: the blocking pull from sequencer to driver (seq_item_portseq_item_export, get_next_item/item_done) which paces stimulus, and the non-blocking broadcast from monitor to subscribers (analysis_port → exports, write()) which fans observation out one-to-many.
  • The full path is sequence → sequencer → driver → DUT → monitor → scoreboard/coverage; every hop except the pins is a TLM transfer you must wire. Missing wires cause a hang (unconnected seq_item_port) or a silent miss (unconnected analysis_port) — both invisible in a correct topology.
  • The durable rule of thumb: build two graphs deliberately — the hierarchy (who owns whom) and the data flow (who sends to whom) — and when an environment builds cleanly but doesn't run right, debug the connect() calls, not the tree.

Next — Reusable Verification Architecture: you now know how components are structured (hierarchy) and how data moves between them (TLM). The next chapter steps back to the design discipline that ties it together — how to architect agents, environments, and connections so the whole verification environment is genuinely reusable across blocks, projects, and teams.