Count the run_phase threads that do the work in the DMA smoke test: two drivers, three monitors, the test. Six threads, started at the same instant, and five of them never finish. (The phaser starts a run_phase thread for every other component too, the env, agents, sequencers and scoreboard, but those return at once or just wait.) Yet the test ends at exactly 655 ns, every time. Something decides that moment, and it is not any of the five loops.
run_phase is a window of simulated time. When it opens, the phaser starts every component's run_phase task as its own thread, all at once and in no particular order. Parents do not start their children's threads, and super.run_phase starts nothing: the phaser forks every component directly.
How long the window stays open is a separate question with a separate owner. A component that needs the window open raises an objection before its work and drops it after. The window closes when the count returns to zero (after an optional drain time). Think of a meeting room booked by whoever is presenting: the projector, the note-taker and the coffee machine are all running, but the meeting ends when the last presenter says "that's all", not when the coffee runs out.
Component | Its run_phase | Holds the window? |
|---|---|---|
| Programs the DMA, sends a packet, waits for the interrupt, checks STATUS | Yes: one objection for the whole scenario |
| Wait for reset, then | No |
|
| No |
| No run_phase: it reacts to | No |
| No run_phase | No |
Objection mechanics (counting, propagation, drain time) are the subject of the Objections Deep Dive. Here the point is ownership: exactly one place decides the duration, and it is the place that knows what "done" means.
// snippet: from the course DMA bench
// Program KEY and LEN, start a job with the interrupt enabled, stream one
// packet of LEN words, wait for the interrupt, then check and clear STATUS.
class dma_smoke_test extends dma_base_test;
`uvm_component_utils(dma_smoke_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
bit [31:0] status, key = 32'hA5A5_0F0F;
int unsigned n_words = 8;
axis_pkt_seq pkt = axis_pkt_seq::type_id::create("pkt");
phase.raise_objection(this, "smoke test running");
reg_write(KEY, key);
reg_write(LEN, n_words);
reg_write(CTRL, 32'h3); // IRQ_EN | GO
if (!pkt.randomize() with { n == n_words; })
`uvm_fatal("RAND", "axis_pkt_seq randomize failed")
pkt.start(env.in_agt.sqr);
wait (irq_vif.irq === 1'b1);
reg_read(STATUS, status);
if (status[1] !== 1'b1) `uvm_error("STATUS", $sformatf("DONE not set: STATUS=%08h", status))
reg_write(STATUS, 32'h2); // write 1 to clear DONE
reg_read(STATUS, status);
if (status[2:0] !== 3'b000) `uvm_error("STATUS", $sformatf("STATUS not clear: %08h", status))
#100ns; // let the last output word drain
phase.drop_objection(this, "smoke test done");
endtask
endclassThe raise is the first statement, before any register write. The writes block until the APB driver comes out of reset, and the objection must already be held while they wait.
wait (irq_vif.irq === 1'b1) is the test's definition of done: the DMA says the job is finished. It is better than any fixed delay because it scales with the job length.
#100ns before the drop lets the last output words leave the DMA and reach the scoreboard. A fixed settle is the simplest end-of-test strategy; the drain test shows a scoreboard that owns the end instead.
Every path to the end reaches the drop. A test with an early return that skips its drop hangs until the timeout.
Drivers do not raise objections. A driver serves whoever is sending items; the sequence or test that sends them knows when the work is done. A driver that raised around each item would also work, but it spreads the definition of "done" across the bench and makes every hang harder to read.
All run_phase threads start at the same simulated time, and their order within that time step is not defined. Nothing about the phase says the APB driver is ready before the test's first register write. The bench does not need it to be: the test's reg_write starts a sequence, the sequence blocks in start_item until the driver calls get_next_item, and the driver does that only after its own reset wait. Order comes from the handshake, not from the phase, and that is also why the test's objection must be raised before the first reg_write: the write may block for the whole reset period. Any cross-component order you need at run time must be built the same way, with a sequencer handshake, a TLM call, an event or a wait on a signal.
UVM_INFO dma_tb.sv(411) @ 655000: uvm_test_top.env.sb [SB] 8 words matched, 0 mismatched
UVM_ERROR : 0
UVM_FATAL : 0655000 ps is the moment the test dropped its objection. The scoreboard's report runs then, in zero time, after every run thread has been killed. If you add a second objection elsewhere, for example a sequence that raises and drops for itself, the window closes at whichever drop comes last.
A test started a background traffic sequence with fork bg.start(env.in_agt.sqr); join_none next to its main register sequence, and raised one objection around the main sequence only. Background traffic was supposed to run for the whole test. In practice it stopped mid-packet the moment the main sequence finished, and the scoreboard reported SB_LEFTOVER for the half-sent packet.
The background sequence never held the window open, so when the main sequence's drop brought the count to zero, run_phase ended and the phaser killed the background thread along with everything else. Two fixes are standard: raise a second objection for the background work before forking it (and drop it when it completes), or give the background sequence its own objection with set_automatic_phase_objection(1). Either way, every independent piece of work that defines "done" must hold its own objection, raised before the last other drop could happen.
super.run_phase(phase). Which threads does that start?None. The base run_phase does nothing (it calls the old run() hook, empty by default). The phaser forks every component's run_phase itself, env and children alike.
#100ns is removed. What risk appears, and what is the more robust alternative?The test may drop while the last output words are still inside the DMA; run ends, and check_phase reports SB_LEFTOVER (or worse, words that were never predicted are never compared). A robust alternative is a scoreboard that extends the phase until every word the DMA owes has come out, as dma_scoreboard_eot does in the drain test.
The drop is never reached, so the count never returns to zero and the test runs until the timeout. Monitors never object; end of test belongs to the test or the scoreboard.
Next: run_phase is one window. Runtime Sub-Phases divides the same time into twelve named stages, and explains why most teams use them only for tests and sequences.