Bound work and prove completion
Put one absolute deadline around stimulus, checking and the quiet window, cancel its timer in the right process scope, and make a stuck test fail fast with a diagnosis.
- Lesson
- U10.3
- Time
- 25 min
- Runs on
- Verilator with UVM
- Check
- 6 questions
A nightly regression job is killed after six hours with no UVM message at all. The design had stopped raising ready after its second transfer; the driver waited for it, the sequence for the driver, and the test for the sequence. UVM's default timeout is 9,200 s of simulated time, nearly 10^12 cycles of a 10 ns clock, so the job's six-hour wall-clock limit killed the run long before UVM would have complained.
| Simulated time | Design | Driver | Sequence | Test |
|---|---|---|---|---|
| 15 ns | Takes 16 | item_done | Sends 32 | Waiting in seq.start |
| 45 ns | Takes 32, then ready stays 0 | item_done | Sends 48 | Waiting |
| 50 ns on | Never ready again | Offering 48, waiting for ready | Waiting in finish_item | Waiting |
| 9,200 s | - | - | - | UVM's default timeout, never reached in a six-hour job |
Each layer waited correctly for the layer below. Nothing put a limit on the total.
Completion first, then a bound
When checking may end is settled in U7.3's section on closing the test: planned counts met, nothing waiting, then a 20 ns quiet window with checking still on. This lesson adds two things: one absolute deadline across stimulus, drain and that window, and the question of which processes the timer and its cancellation reach.
Put one deadline around the whole path
- Arm both branches0 ns (illustrative)
The work branch starts the sequence; the timer branch starts counting. One timer covers everything the work branch will do, not just
seq.start.More context
Both branches of
fork : boundedstart at 0 ns. The work branch's first call isseq.start; the timer branch is a single#500ns.Follow the calls, values and waits
- call seq.start: Work branch → seq.start(sqr)
- Waiting: join_any, then disable bounded — waiting for either branch
- First result checked35 ns (illustrative)
The design accepted 16 at 15 ns; the scoreboard checks 17 at 35 ns.
More context
The two moves are 20 ns apart: the design takes 16 at 15 ns, and the log prints " 35 ns scoreboard y=17 checked".
Follow the calls, values and waits
- data x=16: seq.start(sqr) → Stuck incrementer
- data y=17: Stuck incrementer → Scoreboard
- The second accept45 ns (illustrative)
The design accepts 32 at 45 ns. It is its second accept, and after it the design keeps
readyat 0.More context
So far the run looks healthy: one result checked and one more accepted. The fault shows only in what does not happen next.
Follow the calls, values and waits
- data x=32: seq.start(sqr) → Stuck incrementer
- The design stops accepting50 ns (illustrative)
From 50 ns the driver offers 48 and waits; the sequence waits in
finish_item.More context
The design was built with
STUCK_AFTER = 2. From here the pins do not change: valid stays high with x=48, and ready stays 0.Follow the calls, values and waits
- data x=48: seq.start(sqr) → Stuck incrementer
- Waiting: seq.start(sqr) — in finish_item(48)
- Waiting: Stuck incrementer — ready = 0
- Last available result65 ns (illustrative)
The result of 32 is checked at 65 ns. Two inputs taken, two results checked; the work branch is still inside
seq.start.More context
" 65 ns scoreboard y=33 checked" is the last line before the deadline. Nothing tells the work branch that 48 will never be taken.
Follow the calls, values and waits
- data y=33: Stuck incrementer → Scoreboard
- Waiting: Work branch — still in seq.start
- The deadline wins500 ns (illustrative)
The timer branch finishes, so
join_anyreturns.disable boundedkills the work branch, and the test reports what it found before dropping its objection.More context
The design is still stuck and the driver, another component's process, still offers 48. With a healthy design the work branch finishes at 120 ns instead, and
disable boundedcancels the timer.Follow the calls, values and waits
- return 500 ns: Timer branch → join_any, then disable bounded
- data DEADLINE: join_any, then disable bounded → DEADLINE error
- Released: join_any, then disable bounded
- Released: Work branch
- Released: seq.start(sqr)
Read the diagram as text
- Work branch: Stimulus, results checked, quiet window, final counts
- seq.start(sqr): Sends 16, 32, 48
- Stuck incrementer: ready stays 0 after its second accept
- Scoreboard: Checks 17 at 35 ns, 33 at 65 ns
- Timer branch: #500ns, armed at 0 ns
- join_any, then disable bounded: The first branch to finish wins
- DEADLINE error: 2 of 3 inputs taken, driver offers x=48, ready=0
- Work branch → seq.start(sqr): runs
- seq.start(sqr) → Stuck incrementer: driver offers x
- Stuck incrementer → Scoreboard: monitor reports results
- Work branch → join_any, then disable bounded: never finishes
- Timer branch → join_any, then disable bounded: fires at 500 ns
- join_any, then disable bounded → DEADLINE error: report what is stuck
The run's output: y=17 checked at 35 ns, y=33 checked at 65 ns, then the 500 ns deadline diagnosis and one UVM_ERROR.
Race the complete operation against a timer, and keep the timer alive through stimulus, results checked, the quiet window and the final counts. Bounding only seq.start leaves a second hang: all requests accepted, one result that never arrives.
fork : bounded
begin
seq.start(sqr);
stimulus_finished = 1;
wait (sb.inputs == 3 && sb.checked == 3 && sb.expected.size() == 0);
#(quiet_window); // U7.3's quiet window, checking still on
@(negedge vif.clk);
// Recheck the counts: a late extra result is an error.
finished = 1;
end
begin #500ns; end // the deadline
join_any
disable bounded;
if (!finished) `uvm_error("DEADLINE", "completion did not finish")The example's design stops accepting after two inputs. At 500 ns the diagnostic reads "2 of 3 inputs taken, driver offers x=48, ready=0". With a healthy design the work branch finishes at 120 ns. A missing final response instead reports that stimulus finished, with the checked and waiting counts.
Size the deadline from bounds, not from one run: the healthy path takes 120 ns here, and 500 ns leaves room for this design's fixed pipeline. A design that may stall, queue more work or run a slower clock needs the limit recalculated from those bounds.
Keep the absolute limit even when the quiet window restarts on activity. A faulty design that keeps producing output can then postpone completion forever.
Cancel the timer without killing unrelated work
bounded is a lexical scope, drawn separately with "names" arrows; it is not a process. The background child is a variant (the quiz's scenario): the example has none.- disable bounded
A named disable ends the active branches of the block it names: the work branch and the timer. Both are direct children of
run_phase; the name adds a scope, not a process. A background child is outside that name.More context
The name is how the statement finds the branches. In the stuck example it ends the work branch at 500 ns; in a healthy run it ends the timer.
Follow the calls, values and waits
- call disable bounded: Scope: bounded → Work branch
- call disable bounded: Scope: bounded → Timer branch
- disable fork
disable forkends every descendant of the calling process: both branches and, in the variant, the background traffic started earlier.More context
The reach follows process ancestry, not the text of the fork: the background child came from an earlier
fork ... join_nonebut is still a child ofrun_phase.Follow the calls, values and waits
- call disable fork: Test run_phase → Work branch
- call disable fork: Test run_phase → Timer branch
- call disable fork: Test run_phase → Background traffic
- Inside a helper process
Called inside a helper process,
disable forkreaches only the helper's own descendants. This is the isolated pattern in the details panel, for timed code that other processes may share.More context
The helper's own work and timer children are not drawn. Its
disable forkstops there, so the background child and every other child ofrun_phasekeep running.Follow the calls, values and waits
- call fork the helper: Test run_phase → Helper process
- Outside every form
The driver and monitor are not descendants of this process, so no form stops them. A driver still offering 48 keeps
validhigh.Follow the calls, values and waits
- Waiting: Driver and monitor — driver still offering 48
- wait fork instead
wait forkwaits for every child. Waiting for the timer, a successful run sits until 500 ns, and a timer that reports in its own branch (the quiz variant) fires after a successful run. In the variant it also waits for a forever-running background child and may never return.More context
A separate, successful run. The timer's return reaches
run_phaseonly at 500 ns, and in the variant the forever-running background child never returns, sowait forkstays blocked.Follow the calls, values and waits
- return timer ends: Timer branch → Test run_phase
- Waiting: Test run_phase — in wait fork: still waiting for the background child
- Released: Driver and monitor
Read the diagram as text
- Test run_phase: The calling process
- Work branch: Stimulus and completion checks
- Timer branch: #500ns
- Scope: bounded: Names the two branches of fork : bounded
- Background traffic: Variant: forked earlier with join_none
- Helper process: The isolated pattern: its own work and timer children
- Driver and monitor: Other components' run_phase processes
- Test run_phase → Work branch: spawns
- Test run_phase → Timer branch: spawns
- Test run_phase → Background traffic: spawns (variant)
- Test run_phase → Helper process: spawns
- Scope: bounded → Work branch: names
- Scope: bounded → Timer branch: names
Cancel the timer with a named disable or inside a helper process of its own; never with a wait that includes the timer.
join_any returns when either branch finishes. Record that the work branch finished, then cancel the other branch. wait fork would wait for the timer too, so a timer that reports unconditionally fires after a successful run.
The named disable bounded reaches only that block. disable fork reaches every descendant of the calling process, including background traffic it started earlier. Neither reaches a driver or monitor, whose processes belong to other components.
Isolate a timeout inside a reusable task
A statically named block can have several active invocations. For reusable concurrent code, give the timed operation its own helper process before using disable fork.
fork
begin // the isolated helper process
fork
do_work();
begin #limit; timed_out = 1; end
join_any
disable fork; // descendants of this helper only
end
joinDiagnose before deciding on recovery
The example reports an error and ends. Disabling the work branch stops the wait inside seq.start; it does not lower the driver's valid, flush the request the driver holds, or reset the design. Only stop_sequences() flushes that request; a sequence's own kill() removes it from the arbitration queues but leaves the driver's request in place.
Continuing after a timeout needs the U10.2 procedure. stop_sequences() flushes the request, a reset-aware driver abandons it without item_done, and the design's own cancellation rule updates the scoreboard and models. Never label missing work canceled just to balance the counts.
Simulated-time and wall-clock limits guard different hangs. A simulated-time deadline, local or UVM's global set_timeout, fires only while time advances. A zero-delay loop stops time, so the job runner's wall-clock limit is the only thing that ends it. When the global timeout does expire, it reports fatal PH_TIMEOUT and says only that time ran out.
Global timeout and useful local diagnostics
uvm_root::get().set_timeout(2000ns, 1) sets this module's run-phase backstop; expiry reports fatal PH_TIMEOUT and says only that time ran out. The library default is 9,200 s of simulated time.
A local report should name the stopped stage, the accepted and checked counts, the pending work and any active driver request. Check a request handle for null before reading it: the driver may be idle while the scoreboard waits for a missing output.
Transfer: a design that may stall
Change the design: under backpressure it may legally hold ready low for up to 300 ns at a time, and a test now sends 20 items through a 4-deep queue. Recompute the deadline, and explain why doubling one observed run time is not a bound.
Reason through the deadline
Build the limit from worst cases: every item may meet the full 300 ns stall, plus the pipeline latency, the drain of the queue and the quiet window. A run that happened to see two short stalls says nothing about the next seed.
Report which stage the deadline caught: stimulus still blocked, results still waiting, or extra output during the window. Then inject one permanent stall and one missing result in separate runs; each must end at the deadline with its own diagnosis, and a legal 300 ns stall must not.
Try it
The design was built with STUCK_AFTER = 2, so it never raises ready after its second accept. The sequence waits for 48 forever, and at 500 ns the deadline wins and the test reports what it found.
Two edits turn the run into its other outcomes. Measured on this runner:
| Variant | Edit | What the run prints |
|---|---|---|
| Healthy | Remove #(.STUCK_AFTER(2)) from the design instance | 95 ns scoreboard y=49 checked, then 120 ns test completion checks finished; quiet window over; 0 errors |
| Last result lost | Also change the monitor's if (vif.out_valid) begin to if (vif.out_valid && vif.y != 49) begin | 500 ns test deadline: stimulus_done=1, inputs=3, checked=2, waiting=1, the DEADLINE error and [SB] 1 expected results never arrived; 2 errors |
Healthy: stimulus ends at 75 ns, the last result is checked at 95 ns, the quiet window runs to 115 ns, and the counts are checked at the 120 ns falling edge. Lost result: all three inputs are taken, so the deadline names the missing result instead of a blocked driver.
35 ns scoreboard y=17 checked 65 ns scoreboard y=33 checked 500 ns test deadline: 2 of 3 inputs taken, driver offers x=48, ready=0 UVM_ERROR : 1Check yourself
Remember
- When checking may end is U7.3's rule; this lesson bounds the time it may take.
- Keep one absolute deadline armed through stimulus, results checked, the quiet window and the final counts; report the stalled stage and pending work.
- Size the deadline from worst-case bounds, and keep it even when the quiet window restarts on activity.
- Cancel the timer with a named block or inside its own helper process;
wait forkwaits for the timer, anddisable forkalso reaches earlier background children. - Simulated-time limits fire only while time advances; a wall-clock limit in the job runner covers a stopped scheduler.