We have observed, at a mid-simulation `uvm_fatal(), the resultant $finish can exit the program before all requisite debug information is reported or dumped to a file. For example, we have had wave form tools not dump the current time slice. If the `uvm_fatal() occurred at 100,000 ns, it can be quite disappointing to see waves up to 99,999 ns only. All we needed was one extra clock tick and, without that, debug became more challenging.
In fact, we weren’t the only ones with this issue. At DVCon, a few years ago, during a UVM committee presentation regarding the development of UVM-IEEE an audience member asked the exact same question: will it be possible to delay $finish? The committee was non-committal, but the answer is really: no.
Unless you hack it!
The problem is that the UVM-1.1d library calls $finish in two locations:
uvm_pkg::run_test
— this the task you call to initiate UVM testsuvm_pkg::uvm_report_object::die
— this is the function called at `uvm_fatal() when simulation exit is requested
You must control both locations to properly handle all `uvm_fatal() instantiations resulting in $finish. To complicate matters, UVM-1.2 and UVM-IEEE have consolidated $finish control to uvm_root. These UVM library versions call $finish also in two locations:
uvm_pkg::run_test
— this the task you call to initiate UVM testsuvm_pkg::uvm_root::die
— this is the function called at `uvm_fatal() when simulation exit is requested
Let’s deal with each on its own.
UVM-1.1d
When you call `uvm_info() in your UVM test bench you will always result in calling some uvm_report_object’s functions to handle the message. In Fig.1, below, the uvm_component extends from the uvm_report_object. It is the component instance, then, that handles the composition and action upon the reported message. For `uvm_info(), the action is usually just display.

The flow for `uvm_fatal() is the same as `uvm_info(), but the action differs: display and simulation finish. There is a single line that results in simulation finish in uvm_report_server::process_report() function.
virtual function void uvm_report_server::process_report(..., uvm_report_object client); ... if(action & UVM_EXIT) client.die(); ... endfunction
The actual $finish for UVM-1.1d resides within the uvm_report_object::die() function. If you override the client reference then you control when $finish actually takes affect. We install our own report server to catch the UVM_EXIT condition and override the client.

First, our cvm_report_server extends uvm_report_server and is installed at simulation time 0 with the static uvm_report_server::set_server(…) function. We do this in our uvm_test class extension. Our report server maintains a static singleton reference to our report handler, abt in Fig.2. When required, we override the client with a reference to our handler.
virtual function void cvc_report_server::process_report(..., uvm_report_object client); if((abt != null) && (action & UVM_EXIT)) client = abt; super.process_report(..., client); endfunction
It isn’t enough to simply install our client with an overridden die function. We must also be aware of the active UVM phase. Ideally, we do not want UVM to exit the run_test() task while we delay $finish. Just prior to exiting run_test() it kills all child threads, rendering our delayed $finish moot.
We can raise an objection in the current active phase to prevent exit, but we must do so carefully. That’s because there are two kinds of phases in UVM: function-oriented phases are meant to take zero simulation time for test bench build-up (build, connect, end-of-elaboration, start-of-simulation) and tear-down (extract, check, report, final). Theses function phases ignore all objections. The remaining phases are task-oriented and wait on all objections before moving to the next phase.
Here lies a question for function phases:
Do you want to delay `uvm_fatal during zero-time phases?
If so, then an external thread must be executed outside run_test(). Nicely, a caught `uvm_fatal() instance during one a function-oriented phase will lead immediately to exiting run_test(). As such, it is trivial to call the delay following wherever you already call run_test():
initial begin run_test(); cvm_abort_package::cvm_abort_handler::die_external(); end
As UVM has no built-in function to retrieve the current active phase (or phases as they may execute in parallel), we can implement a monitor. The uvm_component has two functions that enables the monitor nicely. In Fig.3, we have our singleton cvm_phase_monitor override the component’s phase callback hooks.

At phase started we add the current phase to the local static array, m_current_phase. At phase ended, we delete the phase from the array. The result is that m_current_phase contains only active phases. During UVM run-time phases, there will always be at least two: run_phase and some other run-time phase that’s active.
UVM Phases, themselves, are classes instantiated as singletons at the beginning of simulation. These singleton instances are called implementation phases. The schedule of phases in the common and the uvm domains, in Fig.4, are actually representatives of the implementation phase, called node phases. Both are required to properly monitor phase changes and allow for objection by the abort handler object.

The phase_started() virtual function is passed a reference to the node phase. To identify the phase’s domain and the actual implementation phase, it need only:
virtual function void phase_started(uvm_phase phase); uvm_phase imp = phase.get_imp(); uvm_domain dom = phase.get_domain(); ... endfunction
The active phase is added to our m_current_phase associative array at:
m_current_phase[this][dom][imp] = phase;
Of course, it’s never so straight-forward. We employ a three-tiered associative array to ensure we can identify each instance of the monitor and, separately, the domain and phase.
class cvm_phase_monitor extends uvm_component; typedef uvm_phase phase_map_t[uvm_phase]; // 3 typedef phase_map_t domain_map_t[uvm_domain]; // 2 domain_map_t m_current_phase[cvm_phase_monitor]; // 1 virtual function void phase_started(uvm_phase phase); ... if(!m_current_phase.exists(this)) begin domain_map_t mon2domain; // new monitor m_current_phase[this] = mon2domain; end if(!m_current_phase[this].exists(dom)) begin phase_map_t domain2phase; // new domain m_current_phase[this] = domain2phase; end m_current_phase[this][dom][imp] = phase; // received endfunction endclass
In the code above, we build the m_current_phase type with typedefs. The deepest tier of the array (3) maps the phase implementation singleton reference to the phase node reference. The singleton is required to identify if the phase is a task- or function-type phase. The domain tier (2) maps the node phase’s domain to the proper domain. Even though we are considering implementation in the deepest tier, only one implementation phase should be active in one domain at one time. And, finally, the top tier (1) ensures that if this monitor were to be instantiated multiple times it would not step on its own toes. In the phase ended, function, we delete as much as possible to ensure only active phases, domains, and monitors exist.
m_current_phase[this][dom].delete(imp); if(m_current_phase[this][dom].size() == 0) begin m_current_phase[this].delete(dom); if(m_current_phase[this].size() == 0) m_current_phase.delete(this); end
At the end of simulation the m_current_phase associative array should be empty.
Now, we only need to add a (static) function to retrieve any current active phase. We prefer to retrieve a task phase to ensure we can object immediately and disallow phasing to progress. Otherwise, we must retrieve a function phase and delay externally.
static function uvm_phase curr_phase(); uvm_task_phase is_task; uvm_phase result = null; if(!m_current_phase.exists(m)) return null; foreach(m_current_phase[m][dom][imp]) begin if($cast(is_task, imp)) return m_current_phase[m][dom][imp]; else if(result == null) result = m_current_phase[m][dom][imp]; end return result; endfunction
Note that this code does not work with all compilers as it does not strictly adhere to SystemVerilog for foreach indexes. The example code file pulls out each tier into its own foreach loop.
We now have our own custom report server to install our own abort handler when the action on any message is UVM_EXIT and we have a phase monitor to identify the current active phase at abort. We can now implement the abort-handler’s die() function.
class cvm_abort_handler extends uvm_report_object; bit m_external_die = 0; static task delay_finish(); #1ns $finish; endtask static task die_external(); if(m_external_die > 0) delay_finish(); endtask virtual function void die(); uvm_root root = uvm_root::get(); uvm_phase phase = cvm_phase_monitor::curr_phase(); uvm_task_phase is_task; root.m_do_pre_abort(); // required report_summarize(); // required if((phase != null) && ($cast(is_task, phase.get_imp()))) begin // prevent phase exit phase.raise_objection(root); fork delay_finish(); join_none end else begin // prevent run_test() $finish root.finish_on_completion = 0; m_external_die = 1; end endfunction ... endclass
You can expand the delay_finish() task to be more flexible, which we do in the example code. The die() function now looks at the current phase, as retrieved from the cvm_phase_monitor to determine what to do. Recall that task phases are preferred in the curr_phase() function. If the implementation phase retrieved is a task phase extension then we raise an objection on root. This will prevent the phase from exiting and $finish will occur at our delay. If the implementation phase retrieved is actually a function phase, then we disable run_test() automatic $finish and set the internal static flag. Immediately after run_test we always execute die_external(). If the `uvm_fatal() occurred in a function phase then this task will delay the finish outside of the UVM library.
UVM-1.2 / UVM-IEEE
You may be thinking:
I have all the components required, can I use that in UVM-1.2?
No! Hack it!
There are probably more elegant approaches when overriding $finish in UVM-1.2 than our proposal here. However, we believe that those would require a custom uvm_pkg library. We reject that notion. In an effort to maintain portability we should be able to override even the UVM root and core services class singletons directly without a custom uvm_pkg. Unfortunately, we cannot see how that is possible in the current implementation. As such, we prefer to wholesale copy-and-paste the UVM-1.2 library report server’s execute_report_message() function. In two line changes (plus all the implementation from UVM-1.1 presented above) we can port our solution to UVM-1.2.

In Fig.5, there are two immediate differences to our cvm_report_server class compared to the UVM-1.1d version. First, it is now extended from uvm_default_report_server and, second, it overrides the execute_report_message() function. Report messaging has greatly changed in UVM-1.2, pushing most of the message handling directly in to a uvm_report_message class type. However, the die() function has moved from a distributed model, in uvm_report_object, to a centralized model in the uvm_root class. Look in your UVM-1.2 or UVM-IEEE library release from your simulator vendor in the file base/uvm_report_server.svh for the following function:
function void execute_report_message( uvm_report_message message, string composed_message); ... copy and paste ... // Process the UVM_EXIT action if(report_message.get_action() & UVM_EXIT) begin uvm_root l_root; uvm_coreservice_t cs; cs = uvm_coreservice_t::get(); l_root = cs.get_root(); l_root.die(); end ... copy and paste ... endfunction
Copy the whole function as-is into your cvm_report_server class. Look for the above section, specifically, within that function. This is where the root class is called to execute die() and, eventually, call $finish. It is difficult to extend uvm_root properly and implement a new die() function. Given that uvm_top and uvm_root::get() are used interchangeably (we have used both that way and have often seen both used that way), we believe it would be very difficult to ensure proper operation of an extended uvm_root given the uvm_pkg singleton, in uvm_root.svh:
const uvm_root uvm_top = uvm_root::get();
Therefore, we assert it is safer to simply copy-and-paste the execute_report_message() function and insert two lines:
if(report_message.get_action() & UVM_EXIT) begin if(abt == null) begin // insert this line uvm_root l_root; uvm_coreservice_t cs; cs = uvm_coreservice_t::get(); l_root = cs.get_root(); l_root.die(); end else abt.die(); // and insert this line end
With those lines we can achieve the same `uvm_fatal() delay in UVM-1.2 and UVM-IEEE that we do in UVM-1.1d.
Automatic Selection: UVM-1.1d vs UVM-1.2
In both UVM-1.1d and UVM-1.2 we can use the exact same cvm_phase_monitor and cvm_abort_handler. The only difference in the cvm_report_server is the parent class and messaging processing function to override. Everything else is the same. As such, automatic compile-time selection can be made using macro tests, as follows.
package cvm_abort_package; class cvm_phase_monitor extends uvm_component; ... endclass class cvm_abort_handler extends uvm_report_object; ... endclass class cvm_report_server `ifdef UVM_VERSION_1_1 extends uvm_report_server; `elsif UVM_VERSION_1_2 extends uvm_default_report_server; `endif ... `ifdef UVM_VERSION_1_1 virtual function void process_report(...); ... endfunction `elsif UVM_VERSION_1_2 virtual function void execute_report_message(...); ... endfunction `endif endclass endpackage
Example Hack
When we replace the standard UVM report server with one that can handle intercepting `uvm_fatal() and delay the $finish we ensure all tools are allowed time to complete their tasks for the current time slice. This has been been useful to our team.
Example code from DVClub Presentation and this post:
— another hack