Search the bench for #100ns; and you find it at the end of six tests, each time with the same comment: let the last output word drain. Then look at dma_drain_test. It drops its objection the moment the last input word is accepted, with no delay at all, and still compares all six words. Six tests guess how long the DMA takes; one test knows. Which policy should every test follow, and where should it be written?
A UVM run phase ends when no component holds an objection, a counter that says "I am still busy". On the bench each derived test raises one at the start of its run_phase and drops it at the end. That works, but it means nine copies of the end-of-test policy: nine raises, nine drops, six guessed delays. Change the policy, say to wait for the scoreboard instead of a delay, and nine files change.
There are two separate questions inside "when is the test done?". The stimulus is finished: only the test knows that. Nothing the DUT was asked to do is still outstanding: only a checker that watches the DUT knows that. A restaurant closing works the same way. The host stops seating guests (the test drops its objection), the kitchen finishes the orders already taken (the scoreboard holds the phase until they are out), and only then do the lights go off (the drain time, then the cleanup phases).
Most tests own their objection and end with a delay. dma_drain_test is the exception: it relies on a scoreboard override that owns the second question.
// snippet: from the course DMA bench
// The test drops its objection as soon as the last input word is accepted, with
// no settling delay. The end-of-test scoreboard keeps the run phase open until
// the last output word arrives.
class dma_drain_test extends dma_base_test;
`uvm_component_utils(dma_drain_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
dma_scoreboard::type_id::set_type_override(dma_scoreboard_eot::get_type());
super.build_phase(phase);
endfunction
task run_phase(uvm_phase phase);
axis_pkt_seq pkt = axis_pkt_seq::type_id::create("pkt");
phase.raise_objection(this);
reg_write(KEY, 32'h1234_5678);
reg_write(LEN, 6);
reg_write(CTRL, 32'h1); // GO, interrupt not needed
if (!pkt.randomize() with { n == 6; }) `uvm_fatal("RAND", "axis_pkt_seq randomize failed")
pkt.start(env.in_agt.sqr);
phase.drop_objection(this); // the last word is still inside the DMA
endtask
endclassThe override, dma_scoreboard_eot, learns how many words each job owes from the observed LEN and GO register writes. When the run phase's count reaches zero it gets a phase_ready_to_end call, and if words are still owed it raises its own objection until they arrive. phase_ready_to_end is the home of that mechanism; here the point is where the policy lives: in the checker, once, for every test that uses it.
To write the first half once, move the objection into the base test and give derived tests a hook. This is the template method pattern: the base class fixes the order of steps, and derived classes fill in one step.
// snippet
class dma_tmpl_base_test extends dma_base_test;
`uvm_component_utils(dma_tmpl_base_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
// The scenario. Derived tests override this, not run_phase.
virtual task run_scenario();
`uvm_warning("NOSCEN", "run_scenario not overridden: nothing to run")
endtask
task run_phase(uvm_phase phase);
phase.get_objection().set_drain_time(this, 50ns); // margin after the last drop
phase.raise_objection(this, get_type_name());
run_scenario();
phase.drop_objection(this, get_type_name());
endtask
endclass
class dma_smoke2_test extends dma_tmpl_base_test;
`uvm_component_utils(dma_smoke2_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
dma_scoreboard::type_id::set_type_override(dma_scoreboard_eot::get_type());
super.build_phase(phase);
endfunction
task run_scenario();
dma_job_cfg job = dma_job_cfg::type_id::create("job");
bit [31:0] status;
if (!job.randomize() with { len == 8; }) `uvm_fatal("RAND", "dma_job_cfg randomize failed")
run_job(job, 8, status);
if (status[2:1] !== 2'b01) `uvm_error("STATUS", $sformatf("STATUS=%08h", status))
endtask
endclassThe objection carries get_type_name() as its description, so +UVM_OBJECTION_TRACE shows which test raised and dropped it.
set_drain_time replaces the #100ns lines: after the count reaches zero, the phase waits that long before ending. It is a margin, not a latency guess; the scoreboard owns latency.
phase.get_objection() works in UVM 1.2 and IEEE 1800.2. Older code writes phase.phase_done, which IEEE 1800.2 does not define; reference libraries keep it only for compatibility.
Name the hook after what it does. Names like pre_main or post_main invite confusion with the runtime phases pre_main_phase and post_main_phase, which are real phases with their own objections.
A run_scenario that starts its work with fork ... join_none and returns immediately drops the objection at once. If nothing else objects, the run phase ends after the drain time and the forked threads are killed with it: an early end, not a hang. A hook that starts background work must also wait for it, or the work must be owned by something that objects, like the scoreboard above.
Policy | How it ends | Good for | Weakness |
|---|---|---|---|
Fixed delay in each test ( | After a guessed time | A first bench, a quick test | Wrong when latency changes; copied into every test |
Drain time in the base test | A fixed margin after the last drop | Pipelines with short, known latency | Still a guess, but written once |
Checker owns outstanding work | When the scoreboard owes nothing | Any DUT with real latency or backpressure | The checker must learn what is owed from the DUT's inputs |
dma_drain_test drops its objection while the last word is still inside the DMA, and all six words are still compared:
+UVM_TESTNAME=dma_drain_test
UVM_INFO dma_tb.sv(411) @ 375000: uvm_test_top.env.sb [SB] 6 words matched, 0 mismatched
UVM_ERROR : 0Compare it with the smoke test, which matches 8 words at 655000 ps after waiting for the interrupt and then 100 ns more. The drain test ends as soon as the work is done, not 100 ns after a guess.
The first version of the end-of-test scoreboard decided whether to extend the phase by looking at its queue of expected words. While building the bench, dma_drain_test passed with 5 of 6 words compared and no error. The test had dropped its objection in the same timestep as the last input word was accepted, before the input monitor published that word. The queue looked empty, nothing was extended, and the run phase ended with one word never predicted and never compared.
The fix was to count what the DUT was asked to do, from the observed LEN and GO writes, not what the testbench had happened to see so far. End-of-test strategy tells the whole story. For test design, the takeaway is that moving the drop earlier is only safe once a checker owns the second question properly.
dma_timeout_test must still hang until its timeout. Does the template break it?No. Its run_scenario calls run_job with the interrupt disabled, which waits forever, so run_scenario never returns and the objection is never dropped. The 20 µs timeout set in its build_phase still ends the run with PH_TIMEOUT.
axis_sink_agent, what can go wrong, and how would you notice?If the slave stalls the DMA for longer than the drain time after the test drops, the run ends with words still inside the DMA. The plain scoreboard's check_phase reports them as left over (SB_LEFTOVER), so the failure is visible, but it is a testbench failure, not a DUT bug, and it is seed-dependent. A checker that owns outstanding work has no such window.
Next: the policy is written once; now pick which test runs. +UVM_TESTNAME and Plusargs covers selecting tests and knobs from one compiled binary.