A VIP team rewrote their driver to use reset_phase and main_phase, "to be phase-aware". Most tests passed. A few hung forever at the end of main, with the sequencer waiting for an item_done that never came. The driver code was correct line by line. It was in the wrong kind of phase.
The twelve runtime phases split the run window into four named stages, each with a pre, core and post phase: reset, configure, main, shutdown. They run in order on the uvm domain's track, in parallel with run_phase (the schedule picture is in The Phase Schedule). Each phase has its own objection, so a stage ends only when nobody objects to it, and the next stage starts only then.
Think of a stage play with acts. The actors (sequences) perform in their act and leave; the stage crew (drivers and monitors) stays for the whole show. When an act ends, everyone in it is ushered off, mid-sentence if necessary. You would not cast the stage crew as actors in act two.
Stage | Phases | Good for |
|---|---|---|
Reset |
| A test or sequence that applies and releases reset, or waits for it. |
Configure |
| Register programming before traffic, for example through the register model. |
Main |
| The traffic that is the point of the test. |
Shutdown |
| Letting in-flight work finish before checking. |
That is the opening bug. Whenever main ended while the driver was in the middle of an item, the phaser killed the driver's thread, and item_done never happened. A later sequence on the same sequencer then waited forever. Drivers and monitors serve the whole run window, so they live in run_phase, which spans every runtime phase. Tests and sequences use the runtime phases.
The DMA bench handles reset the simple way: each driver drives its idle values, then waits for @(posedge vif.rst_n) at the start of its run_phase. A driver that must survive a reset in the middle of a test watches for it in run_phase and restarts its loop:
// snippet
task run_phase(uvm_phase phase);
forever begin
vif.psel <= 1'b0; // idle while reset is asserted
vif.penable <= 1'b0;
@(posedge vif.rst_n);
fork begin // isolates disable fork to this block
fork
forever begin
seq_item_port.get_next_item(req);
drive(req);
seq_item_port.item_done();
end
@(negedge vif.rst_n); // reset asserted mid-test
join_any
disable fork; // stops the drive loop
end join
end
endtaskThe outer fork begin ... end join matters: disable fork kills every child of the current process, and the wrapper makes the drive loop and the reset watcher the only children it can reach. A real driver also completes or abandons the item it was holding when reset hit, so the sequencer is not left waiting.
The test can then use the stages for its own flow. A sequence that should hold a stage open for as long as it runs can ask for that itself:
// snippet
class dma_cfg_seq extends uvm_sequence #(apb_item);
`uvm_object_utils(dma_cfg_seq)
function new(string name = "dma_cfg_seq");
super.new(name);
set_automatic_phase_objection(1); // raise when started, drop when body ends
endfunction
task body();
// KEY, LEN and CTRL writes
endtask
endclass
// in the test:
task configure_phase(uvm_phase phase);
dma_cfg_seq s = dma_cfg_seq::type_id::create("s");
s.set_starting_phase(phase);
s.start(env.apb.sqr);
endtaskset_automatic_phase_objection(1) makes the sequence raise an objection on its starting phase when it starts and drop it when it finishes. get_starting_phase() and set_starting_phase() replace the old starting_phase member, which is deprecated in UVM 1.2 and removed in IEEE 1800.2. Here the test raises nothing itself; the sequence holds configure open for exactly as long as it runs.
A default sequence starts automatically on a sequencer when a given phase begins, set through config_db with a phase-qualified path such as uvm_config_db#(uvm_object_wrapper)::set(this, "env.apb.sqr.configure_phase", "default_sequence", dma_cfg_seq::type_id::get()). It is convenient for simple flows and opaque for complex ones; many teams prefer explicit starts in the test.
The runtime phases pay off when several independent parts of a bench must reach the same milestone before any of them moves on: every interface out of reset before any configuration, all register programming done before any traffic, every stream drained before checking. Each stage's objection is that barrier, and a sequence can join it without knowing who else is there. They also give a mid-test reset a named place to return to. For a single-interface block bench like the DMA, a straight-line run_phase in the test expresses the same order more readably, and the bench does exactly that: program, stream, wait for the interrupt, check.
In the DMA bench nobody objects in any runtime phase, so all twelve start at time 0 and eleven of them end there, while the traffic runs in run_phase. The exception is post_shutdown: it ends together with run_phase, at 655000, as The Common Phase List explains.
UVM_INFO dma_tb.sv(...) @ 0: uvm_test_top [PH] started uvm.uvm_sched.main
UVM_INFO dma_tb.sv(...) @ 0: uvm_test_top [PH] ended uvm.uvm_sched.main
...
UVM_INFO dma_tb.sv(...) @ 655000: uvm_test_top [PH] ended common.run
UVM_INFO dma_tb.sv(...) @ 655000: uvm_test_top [PH] ended uvm.uvm_sched.post_shutdownWith the configure sequence above, configure would end at the time the last register write completed, main would start right after, and run_phase would still be ending at whatever time the last objection in either track dropped.
A subsystem bench combined two VIPs: one whose test flow used main_phase objections, and one whose sequences raised only in run_phase. The second VIP's traffic ran during configure, because run_phase had started at time 0 and nothing tied its stimulus to the configure stage. The DUT received data before its registers were programmed, and the failures looked like DUT bugs for a week.
The lesson the team wrote down: pick one convention per bench. Either all stimulus is sequenced inside run_phase by the test (the most common choice, and the one the DMA bench uses), or the test and every sequence use the runtime stages consistently. Mixing them means each track's objections gate only their own track.
post_reset_phase. What happens to it?It still runs: the phaser starts and ends it, at the same time if nobody objects. An empty runtime phase costs almost nothing.
configure_phase but raises no objection, and the test raises none either. What happens?configure ends immediately (nobody objects), and the phaser kills the sequence's thread with it, probably before the first write completes. Raise an objection around it, or use set_automatic_phase_objection(1) with set_starting_phase(phase).
Yes. run_phase spans the whole uvm domain's schedule, so the driver's thread lives from time 0 until run ends, serving whichever stage's sequences send it items.
Next: every phase method so far started with super. What super.build_phase() Really Does shows what the library does in those calls, and the bench test that passes when it should not.