← Design Verification Interviews

Interview Question · Code Writing

Ending a Test While Results Are in Flight

L1 JuniorL2 Mid-LevelL3 Senioruvmobjectionsscoreboard

The Question

The interviewer asks

This test drops its objection as soon as the sequence returns. The DUT has a three-clock pipeline. Write an end of test that still checks the last results.

systemverilog
task run_phase(uvm_phase phase);
  three_items seq = three_items::type_id::create("seq");
  phase.raise_objection(this, "three items");
  seq.start(env.sqr);              // returns after the driver's last item_done
  phase.drop_objection(this, "sequence done");
endtask
// The DUT answers y = x + 1 three clocks after it accepts x.
Reveal the Model Answer

The sequence returns when the driver finishes the last item, but two results are still in the pipe. So check_phase sees three sent and one checked. A drain time keeps the phase open a little longer, but it is a guess. A better fix lets the scoreboard object while it still owes results. It raises when its expected queue becomes non-empty and drops when it empties. Keep an end check for anything still outstanding.

Dropping at the last item, with and without a drain time
01234567time (ns)405060708090100110objectionsresults checked0123run, no drainrunendedrun, 50 ns drainrundrainendedlast drop at 60drain ends at 110

Results arrive at 45, 65 and 85 ns. Without a drain time, the run phase ends at 60 with two results still inside the design.

The Scoreboard Objects Only When Results Are Still OwedArrows labelled with a call show who calls whom; the result arrow carries data. The test drops at 60 ns with no drain time; the pipeline still holds two results.
The scoreboard objects only when results are still owedArrows labelled with a call show who calls whom; the result arrow carries data. The test drops at 60 ns with no drain time; the pipeline still holds two results. component Test Drops when itssequence returns counter run objection Total at the root component Scoreboard expected queue: owedresults design Pipeline Three cycles oflatency component Monitor Publishes each result drop_objectionphase_ready_to_endraise /drop_objectiony at out_validwrite(result)
  1. The last drop60 ns (illustrative)

    The sequence returns and the test drops. The total reaches 0, so UVM calls phase_ready_to_end on every component before it ends the phase.

    Follow the calls, values and waits
    1. call test drops: total 0: Test → run objection
    2. call phase_ready_to_end: run objection → Scoreboard
  2. Results are owed: raise60 ns (illustrative)

    Two results are still expected, so the scoreboard raises the objection again and starts a thread that waits for its queue to empty.

    Follow the calls, values and waits
    1. call sb raises: total 1: Scoreboard → run objection
    2. Waiting: Scoreboard — Owes 2 results
  3. The second result65 ns (illustrative)

    The monitor publishes y=21. The scoreboard checks it and now owes one result.

    Follow the calls, values and waits
    1. data y=21: Pipeline → Monitor
    2. data y=21: Monitor → Scoreboard
    3. Waiting: Scoreboard — Owes 1 result
    4. Released: Scoreboard
  4. The last result, then the drop85 ns (illustrative)

    y=31 arrives and the queue is empty. The thread drops at 85 ns; UVM asks again, nothing is owed, and the run phase ends at 85 ns with 3 checked.

    Follow the calls, values and waits
    1. data y=31: Pipeline → Monitor
    2. data y=31: Monitor → Scoreboard
    3. call sb drops: total 0: Scoreboard → run objection
    4. call phase_ready_to_end: run objection → Scoreboard
    5. Released: Scoreboard
Read the diagram as text
  • Test: Drops when its sequence returns
  • run objection: Total at the root
  • Scoreboard: expected queue: owed results
  • Pipeline: Three cycles of latency
  • Monitor: Publishes each result
  • Test → run objection: drop_objection
  • run objection → Scoreboard: phase_ready_to_end
  • Scoreboard → run objection: raise / drop_objection
  • Pipeline → Monitor: y at out_valid
  • Monitor → Scoreboard: write(result)

The end of the test follows the last result at 85 ns, not a guessed drain time.

Self-Check

Tick each key point you covered in your answer.

What Interviewers Listen For (Junior)

A good junior answer sees that "the sequence is done" does not mean "the DUT is done". It names a drain time as the first fix.

Cross-Questions

Where the interviewer goes next. Open a node to practise that follow-up.

What IfProve ItCompareScale It

  1. Ending a Test While Results Are in Flight
  2. CompareDrain Time vs Phase Ready to EndL2 Mid-Level

    Compare a drain time, a scoreboard objection and phase_ready_to_end(). When would you pick each?

    Reveal the Model Answer

    A drain time is a fixed wait after the last drop: simple, but a guess. A scoreboard objection follows the real outstanding work. phase_ready_to_end() is a callback that runs when the count first reaches zero. There a busy component can raise again until it is idle, without objecting during normal traffic.

    Self-Check

    Tick each key point you covered in your answer.

    Half-Truth

    "phase_ready_to_end runs at the end of every phase automatically, so nothing else is needed."

    It is only a hook. The component must still raise an objection inside it, and drop it later, to delay the end.

    Source Verification Academy, UVM Cookbook, End of Test: phase_ready_to_end (PDF p191)

  3. What IfA Ready-to-End Raise That Never DropsL2 Mid-LevelL3 Senior

    What if phase_ready_to_end() raises an objection but never drops it?

    Reveal the Model Answer

    The phase never ends, and the test hangs until the global timeout. So the component must drop the objection when its work is done, on every path, including errors. The callback can also run again each time the count returns to zero, up to a limit. A timeout with a clear message keeps a hang from looking like a slow test.

    Self-Check

    Tick each key point you covered in your answer.

    Misconception

    "phase_ready_to_end() runs once, so raising there is always safe."

    It runs each time the count returns to zero, up to a set number of times. A raise without a drop hangs the phase.

    Source uvm-core 2020.3.1 (IEEE 1800.2 library) source, base/uvm_phase.svh (max_ready_to_end_iterations, default 20)

  4. What IfA Result That Never ArrivesL2 Mid-Level

    What if the DUT loses one result, so the scoreboard's queue never empties?

    Reveal the Model Answer

    The scoreboard's objection never drops, so the test hangs until the timeout. Give the scoreboard a bound: a watchdog that fails after the longest legal latency with the outstanding items listed. That turns a silent hang into a precise error message.

    Self-Check

    Tick each key point you covered in your answer.

    Missed Corner

    "The global timeout will report it."

    It reports a timeout after hours of simulated time, not which item was lost.

    Source uvm-core 2020.3.1 (IEEE 1800.2 library) source, macros/uvm_global_defines.svh, default timeout 9200 s

  5. Scale ItEnding a Test on Many InterfacesL3 Senior

    At subsystem level you have six interfaces and an interrupt that may fire long after traffic stops. Who decides when the test ends?

    Reveal the Model Answer

    No single test can know every latency, so give each checker its own "still busy" rule. Each scoreboard objects or uses phase_ready_to_end() while it owes results. The interrupt checker waits a bounded time for a pending interrupt. The test only owns its stimulus. Add a heartbeat or watchdog, so a stuck component fails with its name instead of hanging.

    Self-Check

    Tick each key point you covered in your answer.

    Wrong Altitude

    "Give the subsystem test one big drain time."

    It hides which component was still busy, and it grows every time someone adds a slower path.

    Source Verification Academy, UVM Cookbook, End Of Test Mechanisms (PDF p190-194)

  6. Prove ItSolve U15.2.1: The Poison Test with a Short LedgerSimulator-graded

    Show it, don't just say it: this Silicon Lab problem is graded by the simulator.

    Open Problem U15.2.1

What People Get Wrong

Common wrong answers and why they fail. Run It shows the wrong belief failing on a real simulator.

Misconception

"When the sequence returns, the DUT has finished."

The sequence returns after the driver's last handshake. The pipeline still holds results, and the phase ends before they come out.

Run It

The interview's test on a three-clock incrementer, with an end check that counts results.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: check_phase at 60 ns: sent=3 checked=1 FAIL: the test dropped its objection when the last item was sent, with 2 results still in the pipe
See the Fix
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;
interface incr_if (input logic clk);
  logic       in_valid = 0, out_valid;
  logic [7:0] x = 0, y;
endinterface
module incr_pipe (incr_if bus);
  logic v[3]; logic [7:0] d[3];
  always_ff @(posedge bus.clk) begin
    v[0] <= bus.in_valid; d[0] <= bus.x + 1;
    v[1] <= v[0];         d[1] <= d[0];
    v[2] <= v[1];         d[2] <= d[1];
  end
  assign bus.out_valid = v[2];
  assign bus.y = d[2];
endmodule
class incr_item extends uvm_sequence_item;
  `uvm_object_utils(incr_item)
  bit [7:0] x, y;
  function new(string name = "incr_item"); super.new(name); endfunction
endclass
class three_items extends uvm_sequence #(incr_item);
  `uvm_object_utils(three_items)
  function new(string name = "three_items"); super.new(name); endfunction
  task body();
    for (int i = 1; i <= 3; i++) begin
      req = incr_item::type_id::create("req");
      start_item(req);
      req.x = 8'(10 * i);
      finish_item(req);
    end
  endtask
endclass
class incr_driver extends uvm_driver #(incr_item);
  `uvm_component_utils(incr_driver)
  virtual incr_if vif;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  task run_phase(uvm_phase phase);
    forever begin
      seq_item_port.get_next_item(req);
      @(negedge vif.clk) begin vif.in_valid <= 1; vif.x <= req.x; end
      @(negedge vif.clk) vif.in_valid <= 0;
      seq_item_port.item_done();
    end
  endtask
endclass
class incr_monitor extends uvm_monitor;
  `uvm_component_utils(incr_monitor)
  virtual incr_if vif;
  uvm_analysis_port #(incr_item) in_ap, out_ap;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase); in_ap = new("in_ap", this); out_ap = new("out_ap", this); endfunction
  task run_phase(uvm_phase phase);
    forever begin
      @(posedge vif.clk);
      if (vif.in_valid) begin incr_item t = incr_item::type_id::create("in"); t.x = vif.x; in_ap.write(t); end
      if (vif.out_valid) begin incr_item t = incr_item::type_id::create("out"); t.y = vif.y; out_ap.write(t); end
    end
  endtask
endclass
`uvm_analysis_imp_decl(_in)
`uvm_analysis_imp_decl(_out)
class incr_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(incr_scoreboard)
  uvm_analysis_imp_in  #(incr_item, incr_scoreboard) in_imp;
  uvm_analysis_imp_out #(incr_item, incr_scoreboard) out_imp;
  bit [7:0] expected[$];
  int sent, checked;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase); in_imp = new("in_imp", this); out_imp = new("out_imp", this); endfunction
  function void write_in(incr_item t); expected.push_back(t.x + 1); sent++; endfunction
  function void write_out(incr_item t);
    bit [7:0] want = expected.pop_front();
    checked++;
    if (t.y !== want) `uvm_error("MISMATCH", $sformatf("y=%0d, expected %0d", t.y, want))
  endfunction
  // The fix: object while results are still owed.
  task run_phase(uvm_phase phase);
    forever begin
      wait (expected.size() > 0);
      phase.raise_objection(this, "results owed");
      wait (expected.size() == 0);
      phase.drop_objection(this, "all results back");
    end
  endtask
endclass
class incr_env extends uvm_env;
  `uvm_component_utils(incr_env)
  uvm_sequencer #(incr_item) sqr;
  incr_driver drv; incr_monitor mon; incr_scoreboard sb;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    virtual incr_if vif;
    if (!uvm_config_db #(virtual incr_if)::get(this, "", "vif", vif)) `uvm_fatal("NOVIF", "no virtual interface")
    sqr = uvm_sequencer #(incr_item)::type_id::create("sqr", this);
    drv = incr_driver::type_id::create("drv", this);
    mon = incr_monitor::type_id::create("mon", this);
    sb  = incr_scoreboard::type_id::create("sb", this);
    drv.vif = vif; mon.vif = vif;
  endfunction
  function void connect_phase(uvm_phase phase);
    drv.seq_item_port.connect(sqr.seq_item_export);
    mon.in_ap.connect(sb.in_imp);
    mon.out_ap.connect(sb.out_imp);
  endfunction
endclass
class incr_test extends uvm_test;
  `uvm_component_utils(incr_test)
  incr_env env;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase); env = incr_env::type_id::create("env", this); endfunction
  task run_phase(uvm_phase phase);
    three_items seq = three_items::type_id::create("seq");
    phase.raise_objection(this, "three items");
    seq.start(env.sqr);
    phase.drop_objection(this, "sequence done");
  endtask
  function void check_phase(uvm_phase phase);
    $display("check_phase at %0t ns: sent=%0d checked=%0d", $time, env.sb.sent, env.sb.checked);
    if (env.sb.checked < env.sb.sent) $display("FAIL: the test dropped its objection when the last item was sent, with %0d results still in the pipe", env.sb.sent - env.sb.checked);
    else $display("PASS: every result came back before the run phase ended");
  endfunction
endclass
module tb;
  bit clk;
  always #5 clk = ~clk;
  incr_if bus (clk);
  incr_pipe dut (bus);
  initial begin
    uvm_config_db #(virtual incr_if)::set(null, "uvm_test_top.env", "vif", bus);
    run_test("incr_test");
  end
endmodule

Prints: check_phase at 85 ns: sent=3 checked=3 PASS: every result came back before the run phase ended

Half-Truth

"Set a large drain time, like 1 ms, to be safe."

It works, but every test in the regression now runs 1 ms longer. It is still a guess if back-pressure stretches the latency.

Source IEEE 1800.2-2020 §10.5.1.3.7 set_drain_time (uvm-core base/uvm_objection.svh)

Half-Truth

"The scoreboard should raise and drop an objection for every item."

Each raise and drop travels up the tree. Objecting only on the empty and non-empty edges does the same job with far fewer calls.

Source Verification Academy, UVM Cookbook, End of Test: transactors cannot object per transaction, for performance (PDF p191)

Learn It, Then Prove It