In a design review, a lead asks you one question about the new block bench: how does a test know it is finished? You can answer "we raise an objection in the test", and you will be asked what happens to the last transaction, what happens when the DUT hangs, and what happens when the same env runs inside the SoC bench, where your test no longer exists. The real question is about strategy, not syntax.
This lesson lays out the five mechanisms UVM gives you, what each one actually guarantees, how each one fails, and how to choose. The worked comparison is the DMA bench itself, which ends two different ways.
Ending a test is like closing a restaurant kitchen. The manager can close when the last order is taken (the test drops when stimulus ends), wait a fixed ten minutes after that (drain time), ask the cooks before turning off the lights (phase_ready_to_end), let the head chef keep the kitchen open while any order is in progress (a scoreboard-held objection), or have someone check every few minutes that the cooks are still moving (a heartbeat). Each answers a different question. The right choice depends on who actually knows that the work is done.
For most designs, the test knows when stimulus is done, and only the checker knows when checking is done. Good strategies combine the two, and add a watchdog for the day neither happens.
Mechanism | Who decides | Guarantees | Fails as |
|---|---|---|---|
Test-held objection | The test | Stimulus ran to completion | Early end if the test drops before the DUT finishes; hang if a wait never returns |
Drain time | A constant | A fixed settle period after the last drop | Lost tail items when latency grows; wasted time on every test when oversized |
phase_ready_to_end hold | A checker, at count zero | The phase cannot end while the checker reports work outstanding | False pass if 'outstanding' is read from a lagging queue; hang if it never drains |
Scoreboard-held objection | A checker, continuously | Phase held from first expected item to last compare | Hang on a lost item; cost and trace noise if raised per item |
Heartbeat or watchdog | A timer | Detects a bench that stopped making progress | Says nothing about correctness; false alarms during legitimately idle periods |
Note what is missing from every row: none of them, alone, both ends at the right moment and protects against a hang. That is why real benches layer them.
The bench ends its tests two different ways, and both pass. Seeing why tells you when to use each.
dma_smoke_test | dma_drain_test | |
|---|---|---|
Who holds the phase | The test, until the interrupt and STATUS checks, plus | The test until the last input word; then |
How 'done' is known | The DUT's done interrupt, observed by the test | LEN words per GO, counted by the scoreboard from the APB monitor |
Tail handling | A fixed 100 ns guess | Exactly as long as words are owed: 10 ns here |
Run ends at | 655 ns (8 words) | 375 ns (6 words) |
Survives at SoC level | No: the block test is replaced by a chip test | Yes: the scoreboard travels with the env |
Weak spot | Tail longer than 100 ns gives SB_LEFTOVER | A lost word hangs until the timeout unless the wait is bounded |
The smoke test is the right shape when the test can observe completion, which the DMA's interrupt allows. Its only flaw is the fixed #100ns for the output pipeline. The drain test is the right shape when the stimulus side cannot know when checking is complete, and it is the one that keeps working when the block env is reused in a bigger bench, because the end-of-test knowledge lives in a component that is reused with it.
A good production bench for this DMA would combine them: the test holds its objection until the interrupt (it owns the scenario), the scoreboard holds the tail with an owed-word count (it owns checking), and set_timeout caps the whole run.
Every bench needs a hang limit. The global timeout ends the run phase with a PH_TIMEOUT fatal: set it with +UVM_TIMEOUT=<time>,<YES|NO> or uvm_root::get().set_timeout(t, overridable). The default is 9200 s of simulated time, far too long to be useful, so set it per test class. dma_timeout_test uses 20 µs:
UVM_FATAL uvm_phase_hopper.svh(598) @ 20000000: reporter [PH_TIMEOUT] Explicit timeout of 20000000 hit, indicating a probable testbench issue
UVM_FATAL : 1A timeout only says too long. A heartbeat says stopped moving, which catches a stalled bench early in a long test. uvm_heartbeat watches a dedicated objection: each monitored component raises and drops it whenever it makes progress, and at every trigger of an event the heartbeat checks the window that just closed. In UVM_ALL_ACTIVE mode every registered component must have been active in the window, otherwise it reports an HBFAIL fatal naming the silent one. The version below needs no change to the agents: a small subscriber on the output monitor's analysis port turns each observed word into a pulse.
// snippet
// One heartbeat pulse per observed output word.
class hb_pulse extends uvm_subscriber #(axis_item);
`uvm_component_utils(hb_pulse)
uvm_callbacks_objection hb_obj;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void write(axis_item t);
hb_obj.raise_objection(this);
hb_obj.drop_objection(this);
endfunction
endclass
class dma_env_hb extends dma_env;
`uvm_component_utils(dma_env_hb)
hb_pulse pulse;
uvm_callbacks_objection hb_obj; // UVM 1.2 needs this type; in IEEE 1800.2 it is a typedef of uvm_objection. Not a phase objection.
uvm_heartbeat hb;
uvm_event hb_tick;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
pulse = hb_pulse::type_id::create("pulse", this);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
hb_obj = new("hb_obj");
pulse.hb_obj = hb_obj;
out_agt.ap.connect(pulse.analysis_export);
hb_tick = new("hb_tick");
hb = new("hb", this, hb_obj);
void'(hb.set_mode(UVM_ALL_ACTIVE));
hb.add(pulse);
endfunction
task run_phase(uvm_phase phase);
hb.start(hb_tick);
forever #10us hb_tick.trigger(); // one window per 10 us
endtask
endclassIn a variant of dma_timeout_test with this env and the timeout raised to 100 µs, the DMA moves its 4 words in the first microsecond and then the bench goes silent, waiting for an interrupt that never comes. The heartbeat fails the run at 20 µs, at the end of its second window, long before the timeout would. With dma_smoke_test the same env passes, because the run is over before the first window closes:
UVM_FATAL @ 20000000: uvm_test_top.env [HBFAIL] Did not recieve an update of hb_obj for component uvm_test_top.env.pulse since last event trigger at time 10000000 : last update time was 315000The first trigger only opens a window; each later trigger checks the window that just closed. The misspelling is the library's own message text. In a block test with idle gaps longer than the window, it raises false alarms; it pays off in long SoC tests where a stalled subsystem would otherwise sit silently for hours.
A block team built their end-of-test into the test: raise, run, wait for the interrupt, drop. It was clean and it passed for a year. Then the block env was reused in the subsystem bench, where a different test with its own virtual sequence drove three DMAs. Subsystem tests began passing with words uncompared, because the subsystem test dropped its objection when its stimulus finished, and nothing in the reused env knew that a DMA's output pipeline was still busy.
The fix moved end-of-test knowledge into the reusable part: the scoreboard learned owed work from the register monitor and held the phase in phase_ready_to_end. The block tests did not change at all. The lesson: put end-of-test knowledge where the knowledge lives, which for checking is the checker, because the test is the one component that is never reused.
A scoreboard hold keyed on outstanding work, in phase_ready_to_end. The test cannot observe completion, and a drain long enough for 2,000 cycles would waste time on every test. Add a bounded wait and the global timeout so a lost transaction becomes an error, not a hang.
Either stop the heartbeat around the idle period (hb.stop(), then hb.start()) or lengthen the window for that test. The heartbeat measures progress, and this test legitimately makes none for a while; the failure is a configuration issue, not a DUT bug.
If end of test lives in the block test, it disappears when the subsystem test replaces it. The subsystem test drops when its stimulus ends, while the block's output pipeline is still busy. Moving the tail hold into the block's scoreboard makes the env carry its own end-of-test knowledge.
Next: at SoC scale, objections themselves start to cost time. Objections at Scale covers the cost model and the tools to control it.