A scoreboard VIP has worked for a year. Then a team instantiates it in a new env with one change: the APB agent is renamed from apb to x_apb to match their naming scheme. Now the scoreboard's build fails to find a setting the agent was supposed to leave for it. Nothing about the code changed except a string. The phaser's visiting order did.
Think of an org chart. When a team is set up, a manager must be hired before they can hire their reports, so setup goes top-down: the parent before its children. When the quarter closes, each report files a status before the manager writes the summary, so close-out goes bottom-up: every child before its parent.
For each function phase the phaser does exactly one of these walks over the component tree. A top-down walk (pre-order) visits a component, then each of its children's subtrees. A bottom-up walk (post-order) visits each child's subtree first, then the component. Task phases do not walk at all: the phaser starts every component's thread at the same time.
Walk | Phases | Why this direction |
|---|---|---|
Top-down |
| A parent creates its children, so it must run first. |
Bottom-up |
| A parent can use what its children prepared: forwarded ports, child audits, child results. |
Parallel |
| Every component's task is forked at once. |
Build numbers grow downward: every parent before its children. Connect numbers grow upward: every child before its parent. Those two facts are guaranteed. The order among siblings is a different matter.
The standard does not say in which order siblings are visited. The Accellera reference library keeps a component's children in an associative array keyed by name, so in practice siblings are visited in lexical order of their names. The DMA topology print shows it: under apb, the children appear as drv, mon, sqr, even though the agent created sqr before drv (its handle is @335, the driver's @458).
That is exactly the trap from the opening. The scoreboard's build read a value that the APB agent's build set, and it worked only because apb sorts before sb. Renamed to x_apb, the agent now builds after the scoreboard. Senior engineers know about name order so that they can recognise this bug, not so that they can rely on it.
A common explanation is that connect is bottom-up "so leaf ports exist before parents connect to them". That is not the reason: by connect time, every build in the tree has finished, so every port exists. Also, connect() does not complete a link on the spot. It records the request, and the library resolves every recorded link in one pass just before end_of_elaboration_phase.
The real reason is handle forwarding. Look at the bench's stream agent:
// snippet: from the course DMA bench
class axis_agent extends uvm_agent;
`uvm_component_utils(axis_agent)
axis_sequencer sqr;
axis_driver drv;
axis_monitor mon;
uvm_analysis_port #(axis_item) ap;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
mon = axis_monitor::type_id::create("mon", this);
if (get_is_active() == UVM_ACTIVE) begin
sqr = axis_sequencer::type_id::create("sqr", this);
drv = axis_driver::type_id::create("drv", this);
end
endfunction
function void connect_phase(uvm_phase phase);
ap = mon.ap;
if (get_is_active() == UVM_ACTIVE)
drv.seq_item_port.connect(sqr.seq_item_export);
endfunction
endclassIts connect_phase sets ap = mon.ap, so the agent exposes one public port and the env never reaches inside it. The env's connect_phase then calls in_agt.ap.connect(sb.in_export). That only works if the agent's connect has already run, and bottom-up order guarantees it: every child's connect finishes before its parent's starts.
A detail worth knowing: the bench's monitors create ap in their constructors, so even a copy made in the agent's build would have worked here. Many monitors create their port in build_phase instead, and then only a connect-time copy is safe. Forwarding in connect works for both styles, which is why it is the convention.
The model below is plain SystemVerilog, not the UVM library. It builds the DMA tree in the bench's creation order, keeps each node's children sorted by name the way the reference library does, and then runs a real recursive pre-order and post-order walk. The checks are computed from the tree, so they test the walk rather than restate it.
class tnode;
string name;
tnode parent;
tnode kids[$];
function new(string n, tnode p);
int i;
name = n;
parent = p;
if (p != null) begin
// Keep children in name order, as the reference library's associative array does.
i = 0;
while (i < p.kids.size() && p.kids[i].name < n) i++;
p.kids.insert(i, this);
end
endfunction
endclass
build (top-down): test env apb apb.drv apb.mon apb.sqr in_agt in.drv in.mon in.sqr out_agt out.mon sb
connect (bottom-up): apb.drv apb.mon apb.sqr apb in.drv in.mon in.sqr in_agt out.mon out_agt sb env test
PASS: 13 nodes, every parent before its children going down and after them going upThe renamed-agent bug has a standard shape: one sibling produces something in its build (a config object, a handle in a shared pool, a config_db entry), and another sibling consumes it in its own build. It works until a rename, a new instance, or a different simulator changes which sibling goes first.
The fix is to move the shared thing up one level. The common parent creates the config object or sets the config_db entry in its own build, which runs before both children, and each child reads it. If the consumer needs a handle to the producer itself, it takes it in connect_phase or later, when every build has finished.
dma_env::build_phase you create the APB agent and, on the next line, read apb.mon. What do you get, and why?null. create only constructs the agent. The agent's build_phase, which creates mon, runs after the env's build_phase returns, because the walk is top-down: the env is visited before its children.
sb) and the APB agent (apb) are siblings. The scoreboard's build reads a setting the agent writes in its build. Why does it work today, and what is the robust fix?It works because the reference library visits siblings in name order and apb sorts before sb. That order is not guaranteed by the standard and changes with names. The robust fix is to have the env (their parent) create or set the shared setting in its own build, before either child builds.
check_phase in the env safely read totals computed by its children's check_phase?check_phase is bottom-up, so every child's check_phase has run before the env's. The same reasoning makes extract and report bottom-up: parents aggregate what children prepared.
Next: walks are how function phases run. Task phases do something quite different, covered in Function Phases vs Task Phases.