This post is a deep dive into UVM objections implementation. Objections are UVM mechanism to control test shutdown(or stopping shutdown really).. It basically keeps things running until all important things stops running(ie 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;

Notes 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)