Look at any driver in the DMA bench: forever begin get_next_item; drive; item_done; end. There is no exit, no stop flag, no break. A junior engineer once asked in a code review: "who stops this loop?" The answers around the table were "the objection", "$finish", and "it just stops". None of them is quite right, and the right one explains a whole class of bugs.
A function phase is like a roll call. The phaser walks the tree once and calls each component's method; each call must return immediately, because a SystemVerilog function cannot wait. When the walk is done, the phase is done. No simulated time passes.
A task phase is a work shift. The phaser starts a separate thread (a SystemVerilog process) for every component's phase task, all at the same moment, and then waits. The shift ends when the last worker who said "I'm busy" has signed out: that sign-in is an objection. When the shift ends, the supervisor sweeps the floor. Every thread the phase started, and every thread those threads forked, is killed, wherever it happens to be.
So the driver's loop is stopped by the phaser killing its thread when run_phase ends. The objection decides when that happens; the kill is how.
The APB monitor is the purest task-phase component in the bench: it loops forever, sampling through the clocking block, and owns no part of deciding when the test ends.
// snippet: from the course DMA bench
class apb_monitor extends uvm_monitor;
`uvm_component_utils(apb_monitor)
virtual apb_if vif;
uvm_analysis_port #(apb_item) ap;
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual apb_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "apb_if not set for this monitor")
endfunction
// A transfer completes on the edge where PSEL, PENABLE and PREADY are all 1.
task run_phase(uvm_phase phase);
forever begin
@(vif.mon_cb);
if (vif.mon_cb.psel && vif.mon_cb.penable && vif.mon_cb.pready) begin
apb_item it = apb_item::type_id::create("it"); // a fresh object per transfer
it.addr = vif.mon_cb.paddr;
it.write = vif.mon_cb.pwrite;
it.data = vif.mon_cb.pwdata;
it.rdata = vif.mon_cb.prdata;
it.slverr = vif.mon_cb.pslverr;
`uvm_info("APB_MON", it.convert2string(), UVM_HIGH)
ap.write(it);
end
end
endtask
endclassIts build_phase is a function: it reads config_db and returns. Its run_phase is a task that never returns. Neither raises an objection. Monitors never do, because a monitor that objected would keep the test alive for as long as the bus exists.
Construct | In a function phase | In a task phase |
|---|---|---|
| Compile error: a function cannot block. | Legal; this is what task phases are for. |
| Compile error: the join blocks. | Legal. |
| Legal. The new thread outlives the function, so it belongs to whatever process called it. | Legal. The thread is killed with the phase. |
|
| Legal; the test or a sequence owns this. |
Starting a sequence | Impossible: | Legal. |
The objection row is worth reading twice. A function phase has no objection object at all, because there is nothing to wait for. Raising one there is not a compile error, so it survives code review; it is caught only when the simulation runs.
The phaser runs each task phase under one parent process and kills that process, with all its descendants, when the phase moves to cleanup. In practice that means:
Every component's run_phase thread, finished or not.
Every thread they forked with join_none, including monitors' helper threads and sequences started from the test.
For a runtime phase such as main_phase, only the threads of that phase. run_phase threads keep going.
The last point is why drivers live in run_phase: a driver written in main_phase is killed when main ends, possibly between get_next_item and item_done. Runtime Sub-Phases tells that story.
Here is a copy of the smoke test (call it dma_nullobj_test) whose build_phase ends with phase.raise_objection(this). It never drives a single bus cycle:
UVM_INFO @ 0: reporter [RNTST] Running test dma_nullobj_test...
UVM_ERROR uvm_phase.svh(1672) @ 0: reporter [UVM/PH/NULL_OBJECTION] 'uvm_test_top' attempted to raise an objection on 'build', however 'build' is not a task-based phase node!
UVM_FATAL @ 0: reporter [BUILDERR] stopping due to build errors
...
UVM_ERROR : 1
UVM_FATAL : 1The raise itself does nothing; it only reports an error. The fatal comes from a second rule: when end_of_elaboration starts, uvm_root checks whether any UVM_ERROR has been reported so far, and if so it stops the run with BUILDERR. Any error during build or connect therefore ends the test at time 0. Elaboration, Start and the Pre-Time-Zero Contract uses that rule on purpose.
A stream monitor was given a counter of observed beats and a closing line: forever begin ... end followed by ¬uvm_info("MON", $sformatf("%0d beats", n), UVM_LOW). The line never appeared. The engineer added +UVM_VERBOSITY=UVM_FULL, then suspected the report server, then the build.
The code was fine; it was unreachable. Nothing ever leaves the forever loop, and when run_phase ended the phaser killed the thread inside it. The same thing happens to any code placed after a wait that never completes, or after a sequence start() that is still running when the phase ends. The fix is to put summaries where they belong: count in run_phase, report in report_phase, which runs after every run thread is gone. That is exactly how dma_scoreboard prints its match count.
fork ... join_none from its build_phase. Is that legal, and what happens to the thread?Legal: join_none does not block. But the thread starts at time 0 before run_phase and is not under run_phase's parent process, so the phaser does not kill it at the end of run, and it is not tied to any objection. Start long-lived threads from run_phase so they live and die with the phase.
fork seq.start(sqr); join_none, raises nothing, and returns from run_phase. What happens to the sequence?Nothing holds run_phase open, so it ends right away (at time 0 here) and the phaser kills the test's thread and every thread it forked, the sequence included. The test ends with no traffic. Raise an objection before forking and drop it when the work is done.
Neither must return. What matters is objections. The monitor holds none, so its loop never keeps the phase alive and is simply killed at the end. The test holds one, so it must drop it; returning is the usual way to reach the drop.
Next: the phaser can only call a method it recognises. Signatures, Super Calls, and Debug shows how one wrong letter makes a phase method invisible.