The DMA spec says software may reset the block in the middle of a job, and the block must come back clean and accept a new one. Your first attempt at the test asserts reset half-way through a 12-word packet. The run never finishes. The APB driver is stuck waiting for a PREADY that a block in reset never gives. The stream driver still holds an item, so the sequencer never grants another. When you force past that, the scoreboard reports words that "never came out", which is true: the DUT threw them away. Three components, three different failures. What does each part of a testbench owe to a reset it did not expect?
A reset in the middle of a job is a fire drill. Nobody finishes what they were doing; everyone does their own small, rehearsed duty and gathers outside, and work restarts only when the all-clear sounds. In a testbench the duties split cleanly by role:
Component | Duty when reset asserts | Duty when reset ends |
|---|---|---|
Driver | Stop driving at once, park the bus, finish the item it holds | Wait for reset to end, then pull the next item |
Monitor | Publish nothing; drop any half-seen transaction | Start sampling fresh |
Scoreboard | Nothing yet | Drop predictions for data the DUT lost; reset its mirror of DUT state |
Stimulus (test or vseq) | Kill the sequences whose work is no longer wanted | Reprogram the DUT and start new sequences |
This is the reset-aware component approach, and it lives entirely in run_phase. The alternative, jumping the runtime phase schedule back to reset_phase, does not touch run_phase components at all; the comparison is in Reset and phase jumps. Most production VIP uses the approach in this lesson.
apb_driver_ra extends the bench's APB driver and replaces only run_phase. The outer forever is one life of the driver: park the bus, wait for reset to be released, then run two threads against each other. One is the normal item loop. The other waits for the falling edge of rst_n. Whichever finishes first ends the join_any, and disable fork kills the other.
// snippet: from the course DMA bench
// Drivers that survive a reset in the middle of a transfer: when rst_n falls they
// stop driving, park the bus, and finish the item they hold, so the sequencer is
// never left waiting for an item_done.
class apb_driver_ra extends apb_driver;
`uvm_component_utils(apb_driver_ra)
bit holding;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
forever begin
vif.psel <= 1'b0;
vif.penable <= 1'b0;
wait (vif.rst_n === 1'b1);
fork begin
fork
forever begin
seq_item_port.get_next_item(req);
holding = 1;
drive(req);
holding = 0;
seq_item_port.item_done();
end
@(negedge vif.rst_n);
join_any
disable fork; // isolated: kills only the two branches above
end join
if (holding) begin
seq_item_port.item_done();
holding = 0;
end
`uvm_info("RST", "APB driver: reset seen, bus parked", UVM_MEDIUM)
end
endtask
endclassThree details carry the whole design:
The isolated fork. disable fork kills every child of the current process, not only the fork just above it. The extra fork begin ... end join wrapper makes a new process whose only children are the two racing threads, so a coverage sampler or a second thread forked earlier by run_phase survives.
item_done for a held item. If reset lands between get_next_item and item_done, the killed loop never calls item_done, and the sequencer still counts that item as in progress. The holding flag records that state, and the driver closes the handshake itself after the fork. Without it, the first get_next_item after reset is the second one without an item_done, and the agent is wedged.
Park first. The loop thread may have been killed half-way through drive(), with PSEL high. The first lines of the next life drive the bus idle before anything else happens.
axis_driver_ra is the same shape around the stream drive code, including the pre_drive hook, so the error-injection overrides still work on top of it.
A driver that survives reset still pulls items from whatever sequence is running. If the packet sequence were left alive, the stream driver would come out of reset and pull the next beat of the old packet. The reset DMA is idle, so that beat would wait at a low s_tready until the next job's GO, then enter the new job as its first word, and the new packet would end up with words left over that no job takes. So the test calls pkt.kill() first. Which stop to use, kill() on one sequence or stop_sequences() on a whole sequencer, and what each leaves behind, is covered in Stopping sequences and reset.
// snippet: from the course DMA bench
// Reset in the middle of a packet: reset-aware drivers park their buses, the
// scoreboard drops what the DUT lost, and a second job afterwards must pass.
class dma_reset_test extends dma_base_test;
`uvm_component_utils(dma_reset_test)
virtual rst_if rst_vif;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
apb_driver::type_id::set_type_override(apb_driver_ra::get_type());
axis_driver::type_id::set_type_override(axis_driver_ra::get_type());
dma_scoreboard::type_id::set_type_override(dma_scoreboard_ra::get_type());
super.build_phase(phase);
if (!uvm_config_db#(virtual rst_if)::get(this, "", "rst_vif", rst_vif))
`uvm_fatal("NOVIF", "rst_if not set for the test")
endfunction
task run_phase(uvm_phase phase);
axis_pkt_seq pkt = axis_pkt_seq::type_id::create("pkt");
dma_job_cfg job = dma_job_cfg::type_id::create("job");
bit [31:0] status;
phase.raise_objection(this);
// Job 1: interrupted by a reset after a few words.
reg_write(KEY, 32'hDEAD_BEEF);
reg_write(LEN, 12);
reg_write(CTRL, 32'h3);
if (!pkt.randomize() with { n == 12; }) `uvm_fatal("RAND", "axis_pkt_seq randomize failed")
fork
pkt.start(env.in_agt.sqr);
join_none
#125ns; // between clock edges, mid-packet
pkt.kill(); // the rest of this packet is no longer wanted
rst_vif.rst_n <= 1'b0;
`uvm_info("RST", "reset asserted in the middle of job 1", UVM_LOW)
repeat (3) @(posedge rst_vif.clk);
rst_vif.rst_n <= 1'b1;
repeat (2) @(posedge rst_vif.clk);
// Job 2: after reset, a whole job must work.
if (!job.randomize() with { len == 5; }) `uvm_fatal("RAND", "dma_job_cfg randomize failed")
run_job(job, 5, status);
if (status[2:1] !== 2'b01) `uvm_error("STATUS", $sformatf("job 2: expected DONE only, STATUS=%08h", status))
#100ns;
phase.drop_objection(this);
endtask
endclassThe test asserts reset 125 ns after the packet starts, deliberately between clock edges, holds it for three clocks, releases it, and then runs a 5-word job that must end with DONE and nothing else. It overrides all three types by factory, so neither the env nor the agents change.
When the DMA resets, every word inside it is gone and KEY returns to 0. The scoreboard must forget the predictions for those words and reset its own copy of KEY, or the first word of the next job is compared against a prediction from the old one. dma_scoreboard_ra gets the reset interface from config_db and does both, on the rising edge of rst_n.
// snippet: from the course DMA bench
// A scoreboard that knows about reset: whatever the DUT held is gone, so the
// predictions for it are dropped, and KEY is back to its reset value of 0.
// It flushes when reset ENDS: a beat can complete on the very edge where reset
// is asserted (the monitor reports it in that timestep), and nothing can
// complete while reset is held, so the release edge is the race-free point.
class dma_scoreboard_ra extends dma_scoreboard;
`uvm_component_utils(dma_scoreboard_ra)
virtual rst_if rst_vif;
int unsigned flushed;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual rst_if)::get(this, "", "rst_vif", rst_vif))
`uvm_fatal("NOVIF", "rst_if not set for the scoreboard")
endfunction
task run_phase(uvm_phase phase);
forever begin
@(negedge rst_vif.rst_n);
@(posedge rst_vif.rst_n);
flushed += expected.size();
`uvm_info("SB_RST", $sformatf("reset: %0d prediction(s) dropped", expected.size()), UVM_LOW)
expected.delete();
key = 32'h0;
end
endtask
endclassThe bench's monitors do not look at reset at all, and on this design they do not need to: the DUT clears m_tvalid asynchronously and the reset-aware driver drops TVALID, so no handshake can be seen during reset. That is a property of this bench, not a rule. A monitor that assembles multi-cycle transactions (an APB setup phase waiting for its access phase, a packet collecting beats) must drop the partial transaction when reset asserts and publish nothing while reset is held, or the scoreboard receives a transfer stitched together from before and after the reset.
// snippet: reset gating in a packet-assembling monitor's run_phase
forever begin
@(vif.mon_cb);
if (!vif.rst_n) begin
if (beats.size() != 0)
`uvm_info("MON_RST", $sformatf("dropped %0d beats of a partial packet", beats.size()), UVM_MEDIUM)
beats.delete(); // reset ends the packet it interrupted
continue; // publish nothing during reset
end
if (vif.mon_cb.tvalid && vif.mon_cb.tready) begin
beats.push_back(vif.mon_cb.tdata);
if (vif.mon_cb.tlast) publish_packet();
end
enddma_reset_test with +UVM_VERBOSITY=UVM_MEDIUM:
UVM_INFO dma_tb.sv(1286) @ 280000: uvm_test_top [RST] reset asserted in the middle of job 1
UVM_INFO dma_tb.sv(961) @ 280000: uvm_test_top.env.apb.drv [RST] APB driver: reset seen, bus parked
UVM_INFO dma_tb.sv(1005) @ 280000: uvm_test_top.env.in_agt.drv [RST] stream driver: reset seen, TVALID dropped
UVM_INFO dma_tb.sv(1035) @ 305000: uvm_test_top.env.sb [SB_RST] reset: 0 prediction(s) dropped
UVM_INFO dma_tb.sv(411) @ 885000: uvm_test_top.env.sb [SB] 8 words matched, 0 mismatched
UVM_ERROR : 0
UVM_FATAL : 0Both drivers react in the same timestep as the reset, at 280 ns. Neither waits for a clock edge.
The scoreboard flushes at 305 ns, the clock edge where the test released reset, three clocks later.
0 predictions dropped in this run: the word in flight had already left the DMA before reset. Had a word still been in the output register, the reset would have destroyed it, its prediction would have been the one dropped here, and without the flush check_phase would have reported SB_LEFTOVER.
8 words matched: 3 from job 1 that came out before reset, and all 5 of job 2. Job 2 finishing with DONE only is the real proof the bench recovered.
The first version of dma_scoreboard_ra flushed on the falling edge of rst_n, which seems obvious: reset starts, forget everything. A test that happened to assert reset right on a clock edge then failed with an SB_UNEXP error: an output word arrived "with nothing expected". The DUT was fine.
The word had completed its handshake on that very edge. The output monitor samples through mon_cb with #1step, so it correctly saw TVALID and TREADY both high just before the edge, and it published the word in that timestep. But the scoreboard's reset thread also woke in that timestep, and it ran first: it flushed the prediction, and the word arrived a moment later to an empty queue. Two processes woke on the same edge, and their order is not defined.
The fix was to move the flush to the end of reset. Nothing can complete a handshake while reset is held, so by the rising edge every word that was going to arrive has arrived, and every prediction still queued belongs to a word the DUT destroyed. The release edge is the race-free point. The test was also changed to assert reset between clock edges, but the scoreboard no longer depends on it.
holding flag and the item_done after the fork. Reset lands while a beat is being driven. What happens after reset?The killed loop never called item_done, so the sequencer still has that item in progress. After reset, the new loop calls get_next_item again without an item_done in between, which the sequencer reports as an error, and no new item is granted: the agent is wedged and the next job hangs until the timeout.
Only the KEY reset. The predictions for words inside the DUT must still be dropped, because those words are still lost, but key = 32'h0 must go, or the scoreboard predicts the next job with the wrong key. The flush policy mirrors exactly what the DUT loses.
fork ... join_any; disable fork; directly in run_phase, and run_phase had earlier forked a thread that counts bus idle cycles. What happens at the first reset?disable fork kills every child of run_phase's process, including the idle-cycle counter, which stops silently. Nothing reports an error; its statistics just stop updating. The fork begin ... end join wrapper limits the kill to the two racing threads.
Next: reset is a signal every component watches for itself. Some signals carry events the test needs to react to. Interrupt agents turns the DMA's interrupt line into an agent.