LRM Bridge: Reports, Tracing, and Introspection
Reporting severity, report handlers, VCD tracing, object hierarchy, attributes, version information, and diagnostics.
How to Read This Lesson
This lesson is an LRM bridge. We translate standard language into the questions you actually ask while debugging and reviewing models.
Diagnostics are a formalized part of the SystemC standard surface. Standardizing error reporting and trace generation ensures interoperability between different IP vendors and testbench infrastructures. Now let's look at how the Accellera kernel implements these facilities natively.
Source and LRM Trail
This chapter is the LRM bridge. The primary reference is Docs/LRMs/SystemC_LRM_1666-2023.pdf; the secondary reference is .codex-src/systemc. Read the LRM first for the rule, then read the source to understand why the rule produces the behavior you see in a debugger.
End-to-End Tracing and Reporting Example
Here is a complete example demonstrates the SC_REPORT macros, custom message types, and generating a standard Value Change Dump (VCD) trace file.
#include <systemc>
SC_MODULE(DiagnosticsDemo) {
sc_core::sc_in<bool> clk;
sc_core::sc_signal<uint32_t> counter{"counter"};
SC_CTOR(DiagnosticsDemo) {
SC_THREAD(logic_thread);
sensitive << clk.pos();
}
void logic_thread() {
uint32_t val = 0;
while(true) {
wait();
val++;
counter.write(val);
// Standard reporting
SC_REPORT_INFO("DiagnosticsDemo", "Counter incremented.");
if (val == 3) {
// Warning with a custom message type ID
SC_REPORT_WARNING("LIMIT_CHECK", "Counter reached 3. Approaching limit.");
}
if (val == 5) {
// Error report. By default, SC_REPORT_ERROR will stop the simulation.
SC_REPORT_ERROR("LIMIT_CHECK", "Counter overflowed the simulated limit!");
}
}
}
};
int sc_main(int argc, char* argv[]) {
sc_core::sc_clock clk("clk", 10, sc_core::sc_time_unit::SC_NS);
DiagnosticsDemo demo("demo");
demo.clk(clk);
// 1. Create a VCD trace file
sc_core::sc_trace_file* tf = sc_core::sc_create_vcd_trace_file("wave_trace");
// 2. Trace signals
sc_core::sc_trace(tf, clk, "System_Clock");
sc_core::sc_trace(tf, demo.counter, "Counter_Value");
std::cout << "Starting simulation. This will terminate automatically on SC_REPORT_ERROR." << std::endl;
// 3. Start Simulation
sc_core::sc_start(100, sc_core::SC_NS);
// 4. Close the trace file gracefully
sc_core::sc_close_vcd_trace_file(tf);
return 0;
}Reports and Handling Policies
Never use std::cout or printf for structural warnings or errors, and never call std::exit() from inside a reusable IP block.
Use the standard macros:
SC_REPORT_INFO(msg_type, msg)SC_REPORT_WARNING(msg_type, msg)SC_REPORT_ERROR(msg_type, msg): By default, throws an exception and halts the scheduler.SC_REPORT_FATAL(msg_type, msg): Immediately aborts execution entirely.
Under the Hood (Accellera Kernel):
When you use a macro like SC_REPORT_WARNING, it generates a sc_report object containing the file, line number, time, and message. This object is passed to a global singleton sc_report_handler::report(). The handler looks up the sc_actions bitmask configured for that specific msg_type and severity. If the action bitmask includes SC_THROW, it literally throws an sc_report exception. If it contains SC_ABORT, it calls std::abort().
Report Handlers
The simulation environment (often the sc_main testbench) can override these default actions. An application can configure all warnings of msg_type "LIMIT_CHECK" to be suppressed using sc_report_handler::set_actions("LIMIT_CHECK", SC_WARNING, SC_DO_NOTHING), or demote an SC_REPORT_ERROR to merely increment an internal counter rather than halting the simulation.
Tracing
Tracing records signal activity into a file.
auto* tf = sc_create_vcd_trace_file("wave");
sc_trace(tf, signal, "signal_name");Trace files must be created before sc_start() is called, and gracefully closed (sc_close_vcd_trace_file) at the end of sc_main().
Under the Hood:
sc_create_vcd_trace_file() instantiates a vcd_trace_file object and pushes it into the sc_simcontext::m_trace_files vector.
During simulation, how do values get dumped? Inside the sc_simcontext::crunch() loop, after the Evaluate and Update phases have finished for the current delta cycle, the kernel iterates over m_trace_files and calls their internal cycle(true) virtual method. This method iterates over all traced signals. If the value has changed since the last delta, it writes the formatted string mapping (e.g., b1011 $n) to the file stream.
Object Hierarchy
SystemC objects know their names and hierarchy. Hierarchy is how a large model remains navigable and debuggable. You can always query an object's location in the elaboration tree via this->name() (e.g., top.router_0.timer_1).
Under the Hood:
Every sc_module and sc_signal derives from sc_object. During construction, they register themselves with sc_simcontext::m_object_manager. This manager links them into a massive tree structure mapping parent objects to their children, enabling tools to introspect the entire topology dynamically.
Deep Dive: Accellera Source for sc_signal and update()
The sc_signal<T> channel perfectly illustrates the Evaluate-Update paradigm of SystemC. In the Accellera source (src/sysc/communication/sc_signal.cpp), sc_signal inherits from sc_prim_channel.
The write() Implementation
When you call write(const T&), the signal does not immediately change its value. Instead, it stores the requested value in m_new_val and registers itself with the kernel:
template<class T>
inline void sc_signal<T>::write(const T& value_) {
if( !(m_new_val == value_) ) {
m_new_val = value_;
this->request_update(); // Inherited from sc_prim_channel
}
}The request_update() call appends the channel to sc_simcontext::m_update_list.
The update() Phase
After the Evaluate phase finishes (all ready processes have run), the kernel iterates over m_update_list and calls the update() virtual function on each primitive channel. For sc_signal, this looks like:
template<class T>
inline void sc_signal<T>::update() {
if( !(m_new_val == m_cur_val) ) {
m_cur_val = m_new_val;
m_value_changed_event.notify(SC_ZERO_TIME); // Notify processes sensitive to value_changed_event()
}
}This guarantees that all concurrent processes see the same old value until the delta cycle advances, perfectly mimicking hardware register delays.
Comments and Corrections