A DMA test asserts reset in the middle of a job to check that the design recovers. The first attempt calls stop_sequences() on the stream sequencer and asserts reset. The simulation dies at once with a fatal from the sequencer. The second attempt drops stop_sequences() altogether, and after reset the old packet sequence carries on where it left off, sending the rest of a packet the DMA no longer expects.
Stopping stimulus cleanly takes three cooperating pieces: the right call to stop the sequence, a driver that lets go of its item properly, and a test that restarts traffic once reset is over. This lesson shows each, with the fatal and the clean version side by side.
When a restaurant closes mid-service, two things must happen. The waiters cancel the open orders, and each cook who is holding a slip puts it back on the pass as finished, even though no plate goes out. If the waiters cancel and the cook keeps working, food comes out for nobody. If the cook drops the slip in the bin without telling anyone, the waiter waits forever. And if the manager throws away every slip on the pass while a cook is still holding one, the cook comes back to a pass that has no record of their order.
That last case is precisely the fatal from the opening. UVM gives you three ways to cancel, and they differ in what they clean up:
|
|
| |
|---|---|---|---|
Stops | This sequence and its children | Every sequence on this sequencer | Every child process of the calling thread |
Removes its waiting requests and locks | Yes | Yes | No (reported later as zombie requests or locks) |
The item the driver already holds | Left in place: the driver must call | Flushed: the driver must not call | Left in place |
Sequence state afterwards |
|
| Unchanged, often stuck at |
Automatic objection | Dropped | Not dropped: the phase stays open | Not dropped |
The driver is the piece every scheme needs. It races each transfer against the reset edge, abandons the transfer if reset wins, and always calls item_done for the item it took. The inner fork ... join_any; disable fork; is wrapped in its own fork begin ... end join, so the disable fork kills only the two racing threads and not other children of the driver (why the guard matters):
// snippet: seq_lab_tb.sv, a reset-aware driver
task run_phase(uvm_phase phase);
forever begin
wait (vif.rst_n === 1'b1); // no new item while in reset
seq_item_port.get_next_item(req);
`uvm_info("DRV", {"got ", req.get_name()}, UVM_LOW)
fork
begin // guard: disable fork below stays in here
fork
repeat (4) @(posedge vif.clk); // stands in for driving the transfer
@(negedge vif.rst_n);
join_any
disable fork;
end
join
if (vif.rst_n !== 1'b1)
`uvm_info("DRV", {"reset: abandoning ", req.get_name()}, UVM_LOW)
seq_item_port.item_done(); // exactly once, reset or not
end
endtaskA real driver also puts the pins into their reset values in the abandon path. The monitor and scoreboard need their own reset handling (flushing half-seen transfers and expected queues); Agent architecture covers those.
// snippet: seq_lab_tb.sv
task run_phase(uvm_phase phase);
lab_burst_seq s1 = lab_burst_seq::type_id::create("s1");
lab_burst_seq s2 = lab_burst_seq::type_id::create("s2");
phase.raise_objection(this);
s1.n = 20;
fork s1.start(env.sqr); join_none
#95ns;
vif.rst_n = 1'b0; // reset in the middle of item s1_1
s1.kill();
`uvm_info("TEST", $sformatf("reset: s1 state=%s", s1.get_sequence_state().name()), UVM_LOW)
#30ns;
vif.rst_n = 1'b1;
s2.n = 2; // a new sequence object after reset
s2.start(env.sqr);
`uvm_info("TEST", "s2 finished after reset", UVM_LOW)
phase.drop_objection(this);
endtaskkill() removes the sequence's waiting requests and locks from the sequencer, calls the sequence's do_kill() hook (override it to release anything the sequence holds, such as a semaphore), kills its children and its body thread, and sets the state to UVM_STOPPED. It does not remove the item the driver is holding, which is why the driver's item_done is still correct, and still needed: until it is called, that stale item stays at the head of the sequencer's request queue. Starting a new sequence object after reset avoids any question about the old one's state.
stop_sequences() kills every sequence registered with the sequencer and then flushes the request FIFO and clears the driver-side bookkeeping. It is not graceful. Its use is a sequencer-wide reset: when the driver itself is told about the reset first and has already called item_done, or when the driver does not call item_done for an item that was flushed under it. Calling stop_sequences() and then letting the driver call item_done is the fatal from the opening.
There is a second trap. stop_sequences() kills each sequence through the sequencer's internal kill, which skips the objection drop that the public kill() performs. A sequence that held an automatic phase objection is stopped, but its objection stays raised, and the phase can no longer end. If a sequence objects, stop it with kill().
Killing a sequence's thread with disable fork skips all of kill()'s bookkeeping. The sequence never reaches UVM_STOPPED; a request it left in the arbitration queue is found and removed later with a SEQREQZMB error, and a lock it held with SEQLCKZMB. If you must time-box a sequence, use the guarded idiom and then call kill() on it, so the sequencer is told.
The lab test above, with the reset-aware driver. Reset lands at 95 ns, while the driver holds s1_1:
UVM_INFO seq_lab_tb.sv(111) @ 22000: uvm_test_top.env.drv [DRV] got s1_0
UVM_INFO seq_lab_tb.sv(111) @ 55000: uvm_test_top.env.drv [DRV] got s1_1
UVM_INFO seq_lab_tb.sv(437) @ 95000: uvm_test_top [TEST] reset: s1 state=UVM_STOPPED
UVM_INFO seq_lab_tb.sv(122) @ 95000: uvm_test_top.env.drv [DRV] reset: abandoning s1_1
UVM_INFO seq_lab_tb.sv(111) @ 125000: uvm_test_top.env.drv [DRV] got s2_0
UVM_INFO seq_lab_tb.sv(111) @ 165000: uvm_test_top.env.drv [DRV] got s2_1
UVM_INFO seq_lab_tb.sv(442) @ 205000: uvm_test_top [TEST] s2 finished after reset
UVM_ERROR : 0
UVM_FATAL : 0Now the same driver with env.sqr.stop_sequences() called just before reset instead of s1.kill():
UVM_INFO seq_lab_tb.sv(111) @ 22000: uvm_test_top.env.drv [DRV] got s1_0
UVM_INFO seq_lab_tb.sv(111) @ 55000: uvm_test_top.env.drv [DRV] got s1_1
UVM_INFO @ 95000: uvm_test_top.env.sqr [uvm_test_top.env.sqr] Sequences stopped. Removing request from sequencer fifo
UVM_INFO seq_lab_tb.sv(122) @ 95000: uvm_test_top.env.drv [DRV] reset: abandoning s1_1
UVM_FATAL @ 95000: uvm_test_top.env.sqr [SQRBADITMDN] Item_done() called with no outstanding requests. Each call to item_done() must be paired with a previous call to get_next_item().
UVM_FATAL : 1And the objection trap: a sequence with set_automatic_phase_objection(1) is the only thing holding the run phase, with a 1 us timeout. Stopping it with stop_sequences() at 60 ns, and then with kill() in a second run:
UVM_INFO seq_lab_tb.sv(601) @ 60000: uvm_test_top [TEST] stop_sequences() at 60ns: g state=UVM_STOPPED
UVM_FATAL uvm_phase_hopper.svh(598) @ 1000000: reporter [PH_TIMEOUT] Explicit timeout of 1000000 hit, indicating a probable testbench issue
UVM_INFO seq_lab_tb.sv(601) @ 60000: uvm_test_top [TEST] kill() at 60ns: g state=UVM_STOPPED
UVM_INFO seq_lab_tb.sv(604) @ 60000: uvm_test_top [END] run phase over at 60000Both runs leave the sequence UVM_STOPPED. Only kill() lets the phase end.
The first attempt in the opening is the second log. stop_sequences() flushed s1_1 from the request FIFO; a moment later the driver, doing the right thing for a reset, called item_done for it, and the sequencer had nothing to pair it with. The team's first instinct was to delete item_done from the reset path, which made the fatal go away and broke every test that reset without calling stop_sequences(). The real fix was to pick one owner for the cleanup: kill() the sequences that were running, and let the driver release its item.
The second attempt, with no stop at all, is the zombie case. The packet sequence was blocked in finish_item; the driver's reset path called item_done, which released it, and it simply sent its next beat after reset. Nothing failed in the testbench; the DMA reported ERR for a packet that started mid-way. A reset test has to stop the old stimulus explicitly, then start new stimulus that begins with reprogramming the registers, because the DUT forgot them.
child.kill() on a sequence that is waiting in start_item (not yet granted). Does the driver need to do anything?No. The child has no item at the driver; its waiting request is removed from the arbitration queue by kill(). Only an item the driver has already received needs the driver's item_done.
wait (vif.rst_n === 1'b1) at the top of its loop rather than right after item_done?It guards every path into get_next_item, including the first one at time 0 and the one right after an abandoned item. Taking a new item while reset is still asserted would start a transfer the DUT cannot see and could pull an item from a sequence that is about to be killed.
fork seq.start(sqr); #10us; join_any disable fork; and starts another sequence on the same sequencer later. Sometimes the log shows SEQREQZMB. Why?When the timeout wins while seq is waiting for a grant, disable fork kills its thread but leaves its request in the arbitration queue. The next arbitration finds a request whose process is dead and removes it with that error. Calling seq.kill() after the disable fork (and guarding the fork) tells the sequencer properly.
Next: a sequence that can be killed may also need to keep the run phase open while it runs. Sequences and objections covers when a sequence should hold an objection, and why the bench lets the test do it.