The DMA bench's end-of-test scoreboard starts its phase_ready_to_end with if (phase.get_imp() != uvm_run_phase::get() || owed == 0) return;. In a review, someone suggests shortening the first test to phase != uvm_run_phase::get(), "since phase already is the run phase". It compiles. The drain test still passes, with one word never checked. To see why, you need to know that there are two different kinds of uvm_phase object.
A festival line-up lists slots: "Stage A, 9 pm". Each slot has its own state (waiting, playing, done) and its own crowd. The song played in that slot exists independently of any slot, and the same song can be played in two slots on two stages. UVM's phases are built the same way.
An imp is the song: one singleton object per phase type, returned by uvm_run_phase::get(), uvm_main_phase::get() and so on. It knows what to call on a component (run_phase, main_phase) and in which walk direction. It has no state and no objection.
A node is the slot: one object per place in a schedule. It has a state, predecessors and successors, an objection count, and a link to its imp. The run phase of the common domain is one node; if you create a second domain, its main is a different node pointing at the same uvm_main_phase imp.
Every phase argument the phaser passes to your code (to run_phase, phase_started, phase_ready_to_end) is a node. uvm_run_phase::get() returns the imp. They are different objects, so phase == uvm_run_phase::get() is never true. To ask "is this the run phase?", compare the node's imp, phase.get_imp() == uvm_run_phase::get(), or its name, phase.get_name() == "run", or use phase.is(uvm_run_phase::get()), which checks both.
You can read a node's state with phase.get_state() and block on one with phase.wait_for_state(UVM_PHASE_ENDED, UVM_GTE). Most testbenches never need to; the value of the picture is knowing which callback runs at which edge.
Before any phase runs, the library builds the common domain: a chain of nodes from build to final. It then adds the uvm domain's schedule of twelve runtime nodes with the run node, which is what makes them start together and end before extract. run_test creates the test, then hands the graph to the phaser. For every node, the phaser does the same steps:
Wait until every predecessor node is done (DORMANT, SCHEDULED, SYNCING).
Start: call phase_started(phase) on every component (STARTED).
Execute: for a function phase, one walk calling the imp's method on each component; for a task phase, fork one thread per component under a single parent process (EXECUTING).
Wait for zero: a task phase stays executing until its objection count has been zero for the drain time. Then phase_ready_to_end(phase) is called on every component; if any of them raises, the node goes back to executing. The loop is capped (20 times by default, set_max_ready_to_end_iterations).
End: call phase_ended(phase) on every component (ENDED), kill the phase's threads and clear its objection (CLEANUP), mark it DONE, and let its successors go.
The three callbacks are called for every phase, common and runtime alike, so code inside them almost always starts by checking which phase it was given.
Here is the bench scoreboard that owns end of test. It uses the node-to-imp comparison exactly as described, raises its own objection when words are still owed, and drops it from a forked thread when they arrive (a fork ... join_none is legal in a function).
// snippet: from the course DMA bench
// A scoreboard that owns end of test. It learns how many words each job owes
// from the observed LEN and GO register writes, and when every objection drops
// while words are still owed, it raises its own objection until they arrive.
// (Counting on the expected queue alone is not enough: the last input word may
// not have been published yet when the test drops its objection.)
class dma_scoreboard_eot extends dma_scoreboard;
`uvm_component_utils(dma_scoreboard_eot)
int unsigned len_reg, owed;
event drained;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
virtual function void write_apb(apb_item t);
super.write_apb(t);
if (!t.write || t.slverr) return;
if (t.addr == LEN) len_reg = t.data[15:0];
if (t.addr == CTRL && t.data[0]) owed += len_reg; // GO: this job owes len_reg words
endfunction
virtual function void write_out(axis_item t);
super.write_out(t);
if (owed > 0) owed--;
if (owed == 0) -> drained;
endfunction
function void phase_ready_to_end(uvm_phase phase);
if (phase.get_imp() != uvm_run_phase::get() || owed == 0) return;
phase.raise_objection(this, $sformatf("%0d word(s) still owed by the DMA", owed));
fork begin
@drained;
phase.drop_objection(this, "all words out");
end join_none
endfunction
endclassTo see the two kinds of object for yourself, a copy of the smoke test adds this to its run_phase:
// snippet
task run_phase(uvm_phase phase);
`uvm_info("PH", $sformatf("node==get(): %0b get_imp()==get(): %0b is(get()): %0b get_name(): %s type: %s",
phase == uvm_run_phase::get(), phase.get_imp() == uvm_run_phase::get(),
phase.is(uvm_run_phase::get()), phase.get_name(), phase.get_phase_type().name()), UVM_LOW)
super.run_phase(phase);
endtaskUVM_INFO dma_tb.sv(...) @ 0: uvm_test_top [PH] node==get(): 0 get_imp()==get(): 1 is(get()): 1 get_name(): run type: UVM_PHASE_NODEnode==get(): 0 is the bug from the opening, measured: the argument is never the imp.
type: UVM_PHASE_NODE confirms what the phaser hands you. uvm_run_phase::get().get_phase_type() would return UVM_PHASE_IMP.
The node's full name is common.run (domain, then phase); a runtime node's is uvm.uvm_sched.main. Use get_name() for short checks and get_full_name() when several domains are in play.
Objections live on nodes too. A copy of the smoke test that raises on the imp, uvm_run_phase::get().raise_objection(this), gets the same error as a raise in build, and the library adds a hint:
UVM_ERROR uvm_phase.svh(1672) @ 0: reporter [UVM/PH/NULL_OBJECTION] 'uvm_test_top' attempted to raise an objection on 'run', however 'run' is not a task-based phase node! (This is a UVM_PHASE_IMP, you have to query the schedule to find the UVM_PHASE_NODE)A copy of the drain test whose scoreboard uses phase != uvm_run_phase::get() instead of the imp comparison shows how quiet this bug is. Compare the two runs:
correct (get_imp() compare):
UVM_INFO dma_tb.sv(411) @ 375000: uvm_test_top.env.sb [SB] 6 words matched, 0 mismatched
UVM_ERROR : 0
node compared with get():
UVM_INFO dma_tb.sv(411) @ 365000: uvm_test_top.env.sb [SB] 5 words matched, 0 mismatched
UVM_ERROR : 0The early return is taken on every call, so the scoreboard never raises. The test drops its objection as soon as the last input word is accepted, nothing objects at ready-to-end, and run_phase ends 10 ns early, with the last word inside the DMA, never predicted and never compared. Both runs report zero errors. Only the match count, six words programmed and five checked, gives it away. The "simplification" removed the only thing keeping the phase open.
The same mistake hides in other forms: if (phase == uvm_main_phase::get()) in phase_started to start a main-phase helper, or a case on phase handles. Each compiles and silently never matches. The habit that prevents all of them is to compare imps or names, never a node against a get().
main nodes exist now, and how many uvm_main_phase imps?Two main nodes (one per domain's schedule) and still one imp. That is the point of the split: the same phase behaviour, placed in two schedules with independent states and objections.
phase_started to print a banner "when run starts". The banner prints twenty-one times. Why, and what is the fix?phase_started is called for every phase node: nine common and twelve runtime. Filter inside it with if (phase.get_imp() == uvm_run_phase::get()) (or phase.get_name() == "run").
phase_ready_to_end raises an objection and immediately drops it in the same call. Does that extend the phase?No. The count goes 0, 1, 0 within the same call, so when the phaser looks again it is still zero and the phase ends. To extend, raise in the callback and drop later, from a thread that waits for the real condition, as dma_scoreboard_eot does.
Next: with the machinery in view, the Build, Connect and Run topic walks each phase of the DMA bench in turn, starting with why build goes top-down.