Objections and premature completion

Keep the run phase open exactly as long as work is in flight, and catch the tests that end too early.

Lesson
U1.3
Time
25 min
Runs on
Verilator with UVM
Check
6 questions

A new test starts a 200-item sequence in its run_phase, and the regression marks it passed. Its log shows the report summary at time 0 with no errors, and no item ever reached the design. Nothing raised an objection, so UVM ended the run phase in the first time step and killed the sequence before it sent anything.

A test that passes in 0 ns
01234time (ns)0102030run phaserunningendedobjectionsitems sentitems planned0123no objection: run ends at 0

The sequence would have sent an item every 10 ns, but the run phase was already over.

An objection keeps the run phase open

The run phase has a counter of objections. phase.raise_objection(this) adds one, and phase.drop_objection(this) takes one away. While the count is above zero, someone still has work to do, so the phase stays open.

When the count returns to zero, the run phase ends, after an optional wait called the drain time. UVM then kills every run_phase task that is still running, such as the driver's and monitor's forever loops. Extract, check and report follow at that same moment.

If nobody raises an objection at all, the count is zero from the start. The run phase ends in the first time step, after the run tasks have started but before any of them waits for a clock. Every check then runs against an empty scoreboard, and an empty scoreboard has no mismatches.

Raise before the work and drop after it

No objection

task run_phase(uvm_phase phase);
  three_items seq = three_items::type_id::create("seq");
  seq.start(env.sqr);
endtask

The run phase ends at 0 ns and kills the sequence. Nothing is sent, nothing is checked, and the test passes.

Raise and drop around the sequence

task run_phase(uvm_phase phase);
  three_items seq = three_items::type_id::create("seq");
  phase.raise_objection(this, "sending 3 items");
  seq.start(env.sqr);
  phase.drop_objection(this, "sequence done");
endtask

The phase stays open while the sequence runs and can end once it returns.

Raise the objection before the first statement that waits, and drop it after the last one. The string argument is a description that appears when you trace objections, so make it say what the work is.

With tracing on, a raise or drop prints one line for the component that made it, with the description and two numbers. count is the objections that component raised itself, and total adds those raised below it. Then it prints one line for each parent whose total changes, ending at uvm_top. The example prints these lines, so you can watch the count go from 1 to 0.

Each raise must be matched by a drop from the same component. A drop with no matching raise stops the test with a fatal error, and a raise with no drop keeps the test alive forever.

Who should raise the objection

The test usually raises it, because the test knows what stimulus it intends to run. A top-level sequence can do the same job for its own body.

Drivers and monitors normally do not object. They run forever, so an objection raised in their loops would either never drop or flip on and off with every item. Raising and dropping per item also slows a large simulation.

A scoreboard is the one checker that may object, because it knows when results are still owed. The section after the drain-time trap shows how it does that without objecting on every item.

Objections add up the tree. When the test raises one, its own count becomes 1, and the total at uvm_top, the hidden root above the test, also becomes 1. The run phase ends when the total at the root reaches zero, wherever in the tree the objections came from.

The drain-time trap

The right-hand version above still has a hole. The sequence returns when the driver has finished the last item, but the design may still be working on it. In the example, the incrementer takes three clock cycles to produce each result.

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.

Without a drain time, the run phase ends at 60 ns. Two results are still in the pipeline, the monitor is killed before it sees them, and the scoreboard has checked one item out of three. If nothing complains about the two missing results, the test passes.

A drain time makes UVM wait after the count reaches zero. phase.get_objection().set_drain_time(this, 50ns); keeps the phase open until 110 ns, so all three results are checked. If anyone raises an objection during the drain, the wait is cancelled and starts again at the next drop.

Drop and trust it

phase.raise_objection(this);
seq.start(env.sqr);
phase.drop_objection(this);
// check_phase: no check for leftovers

Ends at 60 ns with two results unchecked, and passes.

Drain, and fail on leftovers

phase.get_objection().set_drain_time(this, 50ns);
phase.raise_objection(this);
seq.start(env.sqr);
phase.drop_objection(this);
// check_phase: uvm_error if expected.size() != 0

Ends at 110 ns with all three checked; a too-short drain still fails loudly.

A drain time is a guess about the slowest response. If a later design change makes the pipeline deeper, the same guess silently becomes too short. So pair it with a check_phase that reports an error for every expected item that never arrived. Better still, have the test wait until the scoreboard has nothing outstanding before it drops its objection.

Let the scoreboard hold the end

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.

    More context

    The first result, y=11, was checked at 45 ns. The other two are still inside the three-stage pipeline when the sequence returns.

    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.

    More context

    The raise happens inside the hook, in the same time step as the drop, so the total never stays at 0 long enough for the phase to end.

    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.

    More context

    Nothing happens to the objection here: the forked thread is still waiting for expected.size() == 0.

    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.

    More context

    The second call to the hook finds expected empty and returns without raising. With the 50 ns drain instead, the same bench ended at 110 ns.

    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.

phase_ready_to_end is a function every component inherits. UVM calls it each time a phase's objection total reaches zero. If a component raises the objection inside it, the phase stays open; when the total reaches zero again, UVM calls it again. After 20 calls UVM stops asking, but an objection that is never dropped still holds the phase until the timeout.

UVM also calls the hook when nothing ever objected, and once for every phase; the name check keeps the raise to the run phase. In the opening 0 ns failure nothing is owed yet, so this hook cannot replace the test's objection.

A function cannot wait, so the hook forks the waiting thread. Measured: raise at 60 ns, drop at 85 ns, check at 85 ns with 3 sent and 3 checked.
function void phase_ready_to_end(uvm_phase phase);
  if (phase.get_name() != "run" || expected.size() == 0) return;
  phase.raise_objection(this, "results owed");
  fork begin
    wait (expected.size() == 0);
    phase.drop_objection(this, "results owed");
  end join_none
endfunction

Compare this with the drain time. The drain waited a fixed 50 ns and ended at 110 ns; this waits exactly as long as results are owed. If a result never comes, though, the wait never finishes, so keep a timeout and the check_phase leftover check. When the bench cannot know how many results to expect, [U7.3](U7-3-uvm-scoreboard-drain.html) adds a quiet window instead.

Read the count, or let the root sequence object

The objection is an object you can query while the test runs. It returns the same count and total that the trace lines print.

CallWhat it doesMeasured on this lesson's bench
phase.raise_objection(this, "why", n)Adds n (default 1) to this component's count; the description appears in tracesRaised once by the test at 0 ns
obj.get_objection_count(this)Objections this object raised itself1 for the test
obj.get_objection_total(uvm_root::get())Count plus everything below; the phase ends when the root total is 01 at the root
obj.display_objections()Prints the tree of objectors with both numbersShown below
obj.set_drain_time(this, t)Waits t after the total reaches 050 ns in the Try it example

obj is phase.get_objection(). Every call takes the component or sequence that objects.

The test objects for the sequence

phase.raise_objection(this, "sending 3 items");
seq.start(env.sqr);
phase.drop_objection(this, "sequence done");

The test owns the objection; it must remember to drop it on every path. Measured on the same hooked bench: run ends at 85 ns.

The root sequence objects for itself

seq.set_starting_phase(phase);
seq.set_automatic_phase_objection(1);
seq.start(env.sqr);

start() raises before pre_start and drops after post_start. Measured on the bench with the scoreboard's phase_ready_to_end hook and no drain time. All three results are checked by 85 ns.

display_objections() at 1 ns in the automatic version, abridged: the UVM_INFO prefix and dashed separators are left out. The sequence raised it, and the total climbs through its sequencer to uvm_top.
The total objection count is 1
Source  Total
Count   Count   Object
0       1       uvm_top
0       1         uvm_test_top
0       1           env
0       1             sqr
1       1               seq

Use the automatic objection on a root sequence, the one the test starts, so the run lasts exactly as long as its body. Call both setters before start. Without set_starting_phase, the sequence has no phase to object to. Measured: the run ends at 0 ns with 0 sent and 0 checked, no warning and no error.

Child sequences started inside a root sequence do not need their own objection. The root already holds the phase open until every child returns, as long as its body() waits for them (no fork ... join_none left running).

The opposite failure: a test that never ends

A raise without a drop, or a drop that sits behind a wait that never finishes, keeps the run phase open forever. UVM has a global timeout of 9,200 seconds of simulated time, after which it reports a fatal error. With a free-running clock, reaching that time can take hours of real time.

Set a timeout that fits your test, for example uvm_root::get().set_timeout(1ms); before run_test. A hang then becomes a fatal error with a clear message instead of a stuck regression slot.

Try it

The test turns on objection tracing, sets a 50 ns drain time and raises an objection around the sequence. The trace lines show the raise at 0, the drop at 60 and the end of the drain at 110, and the scoreboard reports all three results checked.

Delete the set_drain_time line and run again. The scoreboard now reports two results that never arrived, as a UVM_ERROR. Then delete the raise and drop as well, and watch the test pass at time 0 having checked nothing.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: UVM_INFO @ 0: run_objection [OBJTN_TRC] Object uvm_test_top raised 1 run_objection objection(s) (sending 3 items): count=1 total=1 UVM_INFO @ 60: run_objection [OBJTN_TRC] Object uvm_test_top dropped 1 run_objection objection(s) (sequence done): count=0 total=0 UVM_INFO @ 110: run_objection [OBJTN_TRC] Object uvm_test_top all_dropped 1 run_objection objection(s) (sequence done): count=0 total=0 UVM_INFO main.sv(109) @ 110: uvm_test_top.env.sb [SB] 3 sent, 3 checked, 0 outstanding UVM_ERROR : 0

Check yourself

Predict the outputWhat line does the $display print? UVM's own messages are not part of the answer.
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;
class my_test extends uvm_test;
  `uvm_component_utils(my_test)
  int sent;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  task run_phase(uvm_phase phase);
    repeat (3) begin
      #10 sent++;
    end
  endtask
  function void check_phase(uvm_phase phase);
    $display("check_phase at %0t ns: sent=%0d", $time, sent);
  endfunction
endclass
module tb;
  initial run_test("my_test");
endmodule
Spot the bugThe design takes three cycles to answer, and check_phase fails if any result is still outstanding. Each option is the body of the test's run_phase. Which one fails?
Predict the outputThe bench below is the lesson's three-cycle pipeline, with no drain time. The test drops its objection when the sequence returns at 60 ns. The scoreboard now has the phase_ready_to_end hook from this lesson. Results leave the pipeline at 45, 65 and 85 ns. What does the test's check_phase print?
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;

// The design: y = x + 1, three clock cycles after x is accepted.
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
      incr_item it = incr_item::type_id::create("it");
      start_item(it);
      it.x = 8'(10 * i);
      finish_item(it);
    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) vif.in_valid <= 1; vif.x <= req.x;
      @(negedge vif.clk) vif.in_valid <= 0;
      seq_item_port.item_done();
    end
  endtask
endclass

// Publishes accepted inputs on in_ap and results on out_ap.
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)
// Expects x + 1 for every input and complains about anything still expected at the end.
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
  function void phase_ready_to_end(uvm_phase phase);
    if (phase.get_name() != "run" || expected.size() == 0) return;
    phase.raise_objection(this, "results owed");
    fork begin
      wait (expected.size() == 0);
      phase.drop_objection(this, "results owed");
    end join_none
  endfunction
  function void check_phase(uvm_phase phase);
    if (expected.size() != 0)
      `uvm_error("OUTSTANDING", $sformatf("%0d result(s) never arrived", expected.size()))
    `uvm_info("SB", $sformatf("%0d sent, %0d checked, %0d outstanding", sent, checked, expected.size()), UVM_NONE)
  endfunction
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);
    seq.start(env.sqr);
    phase.drop_objection(this);
  endtask
  function void check_phase(uvm_phase phase);
    $display("check at %0t ns: %0d checked, %0d outstanding", $time, env.sb.checked, env.sb.expected.size());
  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
ConceptWhich component should normally raise the run-phase objection around the stimulus?
ConceptA test uses a 50 ns drain time and passes. A later design change makes the pipeline eight cycles of 10 ns deep instead of three. What happens to the last results?
Read the waveformWith no drain time, how many results has the scoreboard checked when the run phase ends?
Last drop and the results
012345time (ns)455560657585objectionsresult sampleddrop, no drain

Columns are the monitor's sampling instants and the drop. The monitor checks a result at each column where a result is sampled, as long as the run phase is open.

Remember

  • The run phase stays open while its objection count is above zero; with no objection it ends at time 0.
  • The test raises an objection before its stimulus and drops it after; drivers and monitors normally do not object.
  • Dropping as soon as the sequence returns can end the run while results are still inside the design.
  • A drain time is a guess; pair it with a check_phase that fails on outstanding items.
  • Set a timeout so that a missing drop fails quickly instead of hanging.