The DMA spec adds a requirement: software may assert a soft reset in the middle of a job, and the DMA must abandon the job, clear its state and accept a new one. Your test plan needs a test that resets mid-transfer and then runs a clean job. Two engineers propose two designs. One wants phase.jump(uvm_reset_phase::get()). The other says the drivers and monitors should simply notice reset and recover. Who is right?
This lesson explains what a phase jump really does, builds the reset-aware alternative that most production VIP uses, and shows how to choose.
A phase jump is like a stage director stopping a rehearsal and calling "back to the top of act two". The actors go back to an earlier point in the script, but the clock on the wall keeps running. In UVM terms, jump() rewinds a domain's runtime schedule to an earlier phase. Simulation time continues; the phases from the target onwards run again.
The alternative is like actors trained to handle a fire alarm on their own: whatever scene they are in, they stop, clear the stage, wait for the all-clear, and resume. That is a reset-aware component: it watches the reset signal itself, inside run_phase, and needs no help from the schedule.
`phase.jump()` is a function. It records the target and returns. The jump happens when the current phase ends, and the phaser then kills the phase's processes. Code after the call keeps running until the thread next blocks, which is why the listing below returns straight after it.
Objections are cleared. When a phase ends through a jump, UVM clears its objection. A raise left behind in the abandoned pass cannot hang the test, and the re-entered phases start with a count of zero.
Only the domain rewinds. The jump affects the domain whose phase you called it on. Other domains, and run_phase in the common domain, carry on.
// snippet
class dma_soft_reset_test extends dma_base_test;
`uvm_component_utils(dma_soft_reset_test)
int unsigned pass_no;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task main_phase(uvm_phase phase);
phase.raise_objection(this, "TEST: main");
pass_no++;
if (pass_no == 1) begin
start_long_job(); // a job that will be cut short
#2us;
`uvm_info("JUMP", "soft reset mid-job: back to reset_phase", UVM_LOW)
phase.jump(uvm_reset_phase::get());
return; // jump() returns; stop pass 1 here
end
run_clean_job(); // pass 2, after the reset
phase.drop_objection(this, "TEST: main done");
endtask
endclassNotice what jump does not do. The bench's drivers and monitors live in run_phase, so the jump does not touch them. A driver that was half-way through a stream beat is still half-way through it after the jump. Someone still has to make the components recover from reset, which leads to the alternative.
If you do use jump, the components that live in runtime phases must tolerate running a phase twice. Configure sequences must be safe to repeat: they program every register they depend on rather than assuming reset values survived. One-shot flags ("configured", "trained") must be cleared in reset_phase, or the second pass skips work. Scoreboards need a written policy: flush on reset, or keep counting across it. And log a marker such as JUMP right before the call, so the phase trace shows the intent next to the phase that ended early. A jump can also go forwards, for example from main to a shutdown phase when a test detects an unrecoverable error; the same rules apply to whatever it skips.
This is the shape most production VIP uses: the driver's run_phase is a loop that drives idle during reset, runs the normal item loop while reset is high, and abandons it cleanly when reset asserts. It works whether reset comes from the top module, a reset agent or a test, and needs no phase machinery.
// snippet
class axis_driver_ra extends axis_driver; // reset-aware variant of the bench driver
`uvm_component_utils(axis_driver_ra)
bit busy; // between get_next_item and item_done
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
forever begin
vif.tvalid <= 1'b0; // idle while in reset
wait (vif.rst_n === 1'b1);
fork begin // isolates disable fork to the two threads
fork
drive_items();
@(negedge vif.rst_n); // reset asserted mid-test
join_any
disable fork;
end join
if (busy) begin // never leave the sequencer waiting
seq_item_port.item_done();
busy = 0;
end
end
endtask
task drive_items();
forever begin
seq_item_port.get_next_item(req);
busy = 1;
@(posedge vif.clk);
vif.tdata <= req.data; vif.tlast <= req.last; vif.tvalid <= 1'b1;
do @(posedge vif.clk); while (!vif.tready);
vif.tvalid <= 1'b0;
seq_item_port.item_done();
busy = 0;
end
endtask
endclassThe other parts follow the same idea. The monitor drops any partial transaction when reset asserts. The scoreboard flushes its expected queue on reset (the DMA abandons the job, so those words will never come out). The test, or a virtual sequence, stops the stimulus with stop_sequences() on the sequencers, which kills every running sequence and clears the queue (it is not graceful), and starts fresh sequences after reset.
phase.jump to reset | Reset-aware run_phase | |
|---|---|---|
Who notices reset | The test, which calls jump | Every component, from the reset pin |
Components in run_phase | Not affected; must recover anyway | Recover by design |
Objections | Cleared on jump; re-raised in pass 2 | Unchanged; the test holds its own |
Works when reset comes from the DUT or another block | No: the test must know | Yes |
Reuse at SoC level | Every test must coordinate jumps | Free: the agent handles it |
Main cost | Idempotent configure, schedule reasoning | A careful driver, monitor and scoreboard flush |
This is why many teams forbid runtime phases in VIP drivers and monitors altogether. A driver in main_phase is killed when main_phase ends, possibly between get_next_item and item_done, which leaves the sequencer with an item that will never complete. A run_phase driver with explicit reset handling has no such edge.
A team's stream driver lived in main_phase. Their soft-reset test jumped from main back to reset. In pass 2, the first sequence on that agent hung forever in start_item. The objection dump showed the test holding main_phase, waiting on its sequence.
When main_phase ended through the jump, the phaser killed the driver's process after get_next_item and before item_done. The sequencer still considered that item in progress, so the next request was never granted. The fix was the shape above: the driver moved to run_phase with its own reset handling, the jump was removed, and the test drove reset through the reset agent. The test got simpler, and the same agent now survived resets in every other test too.
phase.jump(uvm_reset_phase::get()) and then, on the next line, seq.start(sqr) without returning. Does the sequence start?Yes, it starts, because jump() is a function that returns at once. The sequence runs until the phaser ends the phase and kills the process, which can leave the sequence and driver half-way through an item. Return straight after the jump call.
Typically SB_LEFTOVER (or a count mismatch): the scoreboard predicted words that the reset DMA will never produce. Whichever approach you use, the scoreboard must flush its expected queue when reset asserts.
disable fork in fork begin ... end join?disable fork kills every child of the current process. Without the wrapper it would also kill any other thread run_phase had forked earlier, such as a coverage sampler. The wrapper makes the two threads it races (the item loop and the reset watcher) the only children affected.
Next: some components seem to run in the wrong order, or not at all. Ordering bugs and silent overrides explains the usual reasons.