This post is a deep dive into UVM objections implementation. Objections are a UVM mechanism to control test shutdown (or stopping shutdown really). It basically keeps things running until all important things stop running (i.e., objections are dropped).

raise_objection and drop_objection are used at the start and end of run_phase as follows

55 class test extends test_base;
56    `uvm_component_utils(test)
57
61
62    task run_phase(uvm_phase phase);
65
66       phase.raise_objection(this);
    ...
    ...
87       phase.drop_objection(this);
88    endtask

RANT: Before I started this, it seemed simple enough, but it turned out to be more complicated than I thought. Actually, this is my third time trying to write this.

raise_objection and drop_objection Link to heading

raise_objections is called from phase. So, Jumping to raise_objection defined in uvm_phase.svh.

function void uvm_phase::raise_objection (uvm_object obj,
                                                   string description="",
                                                   int count=1);
  phase_done.raise_objection(obj,description,count);
endfunction

phase_done variable is of type uvm_objection

uvm_objection phase_done; // phase done objection

Jumping to uvm_objection.svh, raise_objection calls m_raise

  virtual function void raise_objection (uvm_object obj=null,
                                         string description="",
                                         int count=1);
    if(obj == null)
      obj = m_top;
    m_cleared = 0;
    m_top_all_dropped = 0;
    m_raise (obj, obj, description, count);
  endfunction

m_raise does a lot of things but mostly it does the bookkeeping for objections. In the snippet below, it increments the m_total_count and m_source_count

 398   function void m_raise (uvm_object obj,
 399                          uvm_object source_obj,
 400                          string description="",
 401                          int count=1);
 402     int idx;
 403     uvm_objection_context_object ctxt;
 404 
 405     if (m_total_count.exists(obj))
 406       m_total_count[obj] += count;
 407     else 
 408       m_total_count[obj] = count;
 409 
 410     if (source_obj==obj) begin
 411       if (m_source_count.exists(obj))
 412         m_source_count[obj] += count;
 413       else
 414         m_source_count[obj] = count;
 415     end

Next, it calls raised which calls comp.raised on that component and triggers m_events

    raised(obj, source_obj, description, count);
 800   virtual function void raised (uvm_object obj,
 801                                 uvm_object source_obj,
 802                                 string description,
 803                                 int count);
 804     uvm_component comp;
 805     if ($cast(comp,obj))
 806       comp.raised(this, source_obj, description, count);
 807     if (m_events.exists(obj))
 808        ->m_events[obj].raised;
 809   endfunction

Enough about raise_objection, Let’s look at drop_objection, It calls m_drop which backtracks everything m_raise does.

 587   function void m_drop (uvm_object obj,
 588                         uvm_object source_obj,
 589                         string description="",
 590                         int count=1,
 591                         int in_top_thread=0);
 592
 600
 601     if (obj == source_obj) begin
 602       if (!m_source_count.exists(obj) || (count > m_source_count[obj])) begin
 603         if(m_cleared)
 604           return;
 605         uvm_report_fatal("OBJTN_ZERO", {"Object \"", obj.get_full_name(),
 606           "\" attempted to drop objection '",this.get_name(),"' count below zero"});
 607         return;
 608       end
 609       m_source_count[obj] -= count;
 610     end
 611
 612     m_total_count[obj] -= count;
 613

The important part is updating m_scheduled_list. I will circle back to this.

m_scheduled_list.push_back(ctxt);

Test Termination Link to heading

So far, I don’t know how the objection stops test termination. Naturally, I look into uvm_phase.svh to understand the phase execution.

uvm_phase::execute_phase forks several threads, One of those threads is WAIT_FOR_ALL_DROPPED. basically, it checks m_top_all_dropped and goes into wait_for waiting for UVM_ALL_DROPPED

1197                  if (!phase_done.m_top_all_dropped)
1198                    phase_done.wait_for(UVM_ALL_DROPPED, top);

wait_for is waiting for m_events to get all_dropped

 880      m_events[obj].waiters++;
 881      case (objt_event)
 882        UVM_RAISED:      @(m_events[obj].raised);
 883        UVM_DROPPED:     @(m_events[obj].dropped);
 884        UVM_ALL_DROPPED: @(m_events[obj].all_dropped);
 885      endcase
 886

all_dropped is the task that sends m_events[obj].all_dropped trigger

 829   // Function: all_dropped
 830   //
 831   // Objection callback that is called when a <drop_objection> has reached ~obj~,
 832   // and the total count for ~obj~ goes to zero. This callback is executed
 833   // after the drain time associated with ~obj~. The default implementation
 834   // calls <uvm_component::all_dropped>.

 836   virtual task all_dropped (uvm_object obj,
 837                             uvm_object source_obj,
 838                             string description,
 839                             int count);
 840     uvm_component comp;
 841     if($cast(comp,obj))
 842       comp.all_dropped(this, source_obj, description, count);
 843     if (m_events.exists(obj))
 844        ->m_events[obj].all_dropped;
 845     if (obj == m_top)
 846       m_top_all_dropped = 1;
 847   endtask

What calls all_dropped? I will trace from the end here and work my way backward.

all_dropped is called from m_forked_drain

all_dropped(obj,source_obj,description, count)

and m_forked_drain is called from m_execute_scheduled_forks

 697 `endif
 698                       // Execute the forked drain
 699                       objection.m_forked_drain(ctxt.obj, ctxt.source_obj, ctxt.description, ctxt.count, 1);

m_execute_scheduled_forks waits for m_scheduled_list to have something.

 662   // background process; when non
 663   static task m_execute_scheduled_forks();
 664     while(1) begin
 665       wait(m_scheduled_list.size() != 0);
 666       if(m_scheduled_list.size() != 0) begin
 667           uvm_objection_context_object c;
 668           uvm_objection o;

Note that m_execute_scheduled_forks is called from m_init_objections, which is called from:

 764   static function void m_init_objections();
 765     fork
 766       uvm_objection::m_execute_scheduled_forks();
 767     join_none
 768   endfunction
319 task uvm_root::run_test(string test_name="");
320
332   // Set up the process that decouples the thread that drops objections from
333   // the process that processes drop/all_dropped objections. Thus, if the
334   // original calling thread (the "dropper") gets killed, it does not affect
335   // drain-time and propagation of the drop up the hierarchy.
336   // Needs to be done in run_test since it needs to be in an
337   // initial block to fork a process.
338   uvm_objection::m_init_objections();

Back to m_dropm m_scheduled_list is updated with ctxt which activates the logic in m_execute_scheduled_forks

m_scheduled_list.push_back(ctxt)