Chapter 6: Practice

Testing, Tracing, and Debugging SystemC Models

How to test modules, generate VCD traces, debug TLM transactions, and make failures explain themselves.

How to Read This Lesson

Treat this as engineering practice, not trivia. The patterns here are the ones that keep large models understandable after the original author has moved on.

SystemC models should be testable like software and observable like hardware. A good model does not merely run; it tells you what it did, when it did it, and why it rejected bad input.

Source and LRM Trail

Practice lessons should still cite their roots. Use Docs/LRMs/SystemC_LRM_1666-2023.pdf for behavior, .codex-src/systemc for the reference kernel, and .codex-src/systemc-common-practices for reusable modeling patterns. The goal is to turn standard rules into habits that survive real project scale.

End-to-End Tracing Example

Below is a complete, compilable example demonstrating structural testing, VCD tracing, and the use of SC_REPORT_* macros for effective debugging.

#include <systemc>
#include <iomanip>
 
using namespace sc_core;
 
SC_MODULE(Counter) {
  sc_in<bool> clk{"clk"};
  sc_in<bool> rst{"rst"};
  sc_out<int> count{"count"};
 
  SC_CTOR(Counter) {
    SC_METHOD(tick);
    sensitive << clk.pos();
    dont_initialize();
  }
 
  void tick() {
    if (rst.read()) {
      count.write(0);
      SC_REPORT_INFO(name(), "Reset active, count cleared.");
    } else {
      int next_val = count.read() + 1;
      count.write(next_val);
      if (next_val > 10) {
        SC_REPORT_WARNING(name(), "Count exceeded expected maximum of 10.");
      }
    }
  }
};
 
int sc_main(int argc, char* argv[]) {
  sc_clock clk("clk", 10, SC_NS);
  sc_signal<bool> rst("rst");
  sc_signal<int> count("count");
 
  Counter dut("dut");
  dut.clk(clk);
  dut.rst(rst);
  dut.count(count);
 
  // 1. Setup VCD Tracing
  sc_trace_file* tf = sc_create_vcd_trace_file("wave");
  tf->set_time_unit(1, SC_NS);
  sc_trace(tf, clk, "clk");
  sc_trace(tf, rst, "rst");
  sc_trace(tf, count, "count");
 
  // 2. Executable Test Sequence
  rst.write(true);
  sc_start(25, SC_NS); // Hold reset
  
  rst.write(false);
  sc_start(100, SC_NS); // Run normal counting
 
  // 3. Automated State Check
  if (count.read() != 10) {
    SC_REPORT_ERROR("Testbench", "Counter did not reach expected value of 10.");
    sc_close_vcd_trace_file(tf);
    return 1;
  }
 
  sc_close_vcd_trace_file(tf);
  return 0;
}

Executable Tests

Keep your tests deterministic. Avoid tests that depend on wall-clock time, random ordering, or host-specific output formatting. Small targeted models test reset behavior, FIFO occupancy, interrupt logic, or transaction status much faster than large integration tests.

Transaction Tracing

For TLM platforms, transaction logs are often more useful than waveforms. A good transaction log includes the initiator name, target name, command, address, data length, byte-enable state, response status, annotated delay, and simulation timestamp.

// Inside a b_transport callback:
std::ostringstream msg;
msg << "WRITE addr=0x" << std::hex << trans.get_address()
    << " len=" << std::dec << trans.get_data_length()
    << " delay=" << delay;
SC_REPORT_INFO(name(), msg.str().c_str());

Debugging Delta Cycles

When a signal appears one step late, remember that signal writes are deferred until the update phase. To prove a delta-cycle issue, you can temporarily add diagnostic prints before and after wait(SC_ZERO_TIME);. However, do not fix delta-cycle bugs by scattering zero-time waits everywhere. First, understand which process writes, which channel updates, and which event wakes the reader according to the LRM scheduling phases.

Failure Messages

The best failure message answers four questions:

  1. Which model object failed?
  2. What simulated time was it?
  3. What operation was attempted?
  4. What rule was violated?

Using SC_REPORT_ERROR(name(), msg.str().c_str()); is much better than assert(false). Assertions crash the simulator abruptly, while reports use the SystemC diagnostic system, allowing users to override actions, catch the error, or downgrade it to a warning.

Under the Hood: How sc_trace Works

When you call sc_trace(tf, signal, "name"), SystemC uses the sc_trace_file API (e.g., vcd_trace_file in sysc/tracing/sc_vcd_trace.cpp). You might wonder how the trace file knows the signal's value changed. SystemC does not fire a callback on every signal write. Instead, sc_trace stores a pointer to the variable's memory address. At the very end of the delta cycle (after the update phase), if tracing is enabled, the kernel calls sc_trace_file::cycle(). The tracer iterates over all registered variable pointers, compares their current memory value against their previous cached value, and if different, writes a value change to the .vcd file.

IEEE 1666-2023 LRM: Reports, Tracing, and Diagnostics

The SystemC standard provides a formalized diagnostic system (LRM Chapter 8) and tracing system (LRM Chapter 8.3). Understanding these formalisms allows you to build robust, production-quality simulation environments.

The Report Handling System (LRM 8.2)

The SC_REPORT_* macros are not just glorified std::cout statements. They are entry points into a highly configurable diagnostic routing system.

Message Types and Severities (LRM 8.2.1)

The LRM defines four severity levels:

  • SC_INFO: Informational messages. Simulation continues.
  • SC_WARNING: Potential problems. Simulation continues.
  • SC_ERROR: Recoverable errors. By default, throws an exception or halts.
  • SC_FATAL: Unrecoverable errors. Simulation immediately aborts.

Every report has a Message Type (a string, like "Testbench" or "TLM-2") and a Severity.

Actions (LRM 8.2.2)

When a report is generated, the kernel determines what to do based on the configured Actions (sc_actions). The standard actions are:

  • SC_DO_NOTHING
  • SC_THROW: Throws an sc_report C++ exception.
  • SC_LOG: Writes to the simulation log.
  • SC_DISPLAY: Prints to standard output.
  • SC_CACHE_REPORT: Saves the report so it can be retrieved via sc_report_handler::get_cached_report().
  • SC_INTERRUPT: Drops the user into the debugger (via an architecture-specific interrupt trap).
  • SC_STOP: Calls sc_stop() to cleanly end the simulation.
  • SC_ABORT: Aborts the program immediately (e.g., via abort()).

The sc_report_handler (LRM 8.2.3)

The sc_report_handler is the static router for all diagnostics. You can configure it to change the default actions based on Severity, Message Type, or a combination of both.

For example, if a specific 3rd-party IP model generates too many "TLM-2" warnings, you can suppress them in your testbench:

sc_report_handler::set_actions("TLM-2", SC_WARNING, SC_DO_NOTHING);

Or, you can force the simulator to drop into GDB whenever any error occurs:

sc_report_handler::set_actions(SC_ERROR, SC_DISPLAY | SC_INTERRUPT);

You can even replace the entire report handler with your own custom C++ function (LRM 8.2.6) using sc_report_handler::set_handler(). This is incredibly useful for integrating SystemC logs into Python testing frameworks or standard corporate logging infrastructure (like Log4cxx).

VCD Tracing Formalisms (LRM 8.3)

The standard explicitly defines the behavior of Value Change Dump (VCD) tracing.

  • Trace Files (sc_trace_file): Tracing is initialized by sc_create_vcd_trace_file().
  • Time Units: You must explicitly set the timescale via set_time_unit(). If you do not, the LRM states it defaults to 1 picosecond (which might bloat your waveform file with excessive precision).
  • Registration Rules: Variables and signals must be registered with the trace file before simulation begins (i.e., before sc_start() is called). Attempting to call sc_trace while the simulation is running results in undefined behavior (and typically throws an error in the Accellera implementation).
  • Delta vs Timed Tracing: By default, SystemC trace files record values at the end of a time step (after all delta cycles have settled). However, sc_trace_file::delta_cycles(true) can be called to dump a state change for every single delta cycle. This is an advanced debug technique when tracking down combinational logic loops.

Phase Callbacks for Introspection (LRM 4.1.4, 4.3.4)

When debugging, sometimes you need to walk the module hierarchy or inject code right before simulation starts. The LRM defines phase callbacks that every sc_module and sc_channel can override:

  • void before_end_of_elaboration()
  • void end_of_elaboration(): All ports are bound. You can traverse the hierarchy here.
  • void start_of_simulation(): Called right before the Initialization Phase. Excellent for opening files, initializing external debug sockets, or setting up initial state.
  • void end_of_simulation(): Called when sc_stop() finishes. Used for closing files, flushing trace buffers, and printing final statistics.

Deep Dive: Accellera Source Code

The Tracing Implementation (sysc/tracing)

If you look at sysc/tracing/sc_vcd_trace.cpp, you'll see how sc_trace actually captures C++ variables without the variables knowing they are being traced.

When you call sc_trace(tf, my_int, "my_int"), the trace file dynamically allocates a subclass of sc_trace_file_base::vcd_trace. For integers, it creates a vcd_T_trace<int>. This object stores:

  1. A const int* pointer to the actual memory address of my_int.
  2. An int representing the "previous value".

The SystemC kernel holds a list of all active trace files. At the end of sc_simcontext::crunch() (the simulation loop), if time has advanced, it calls tf->cycle(). The cycle() function loops over every registered vcd_T_trace. It dereferences the pointer to read the current memory value, compares it to the previous value, and if it has changed, it writes the string representation (e.g., binary 1010) to the .vcd file.

This means you can trace any vanilla C++ variable in your module, not just sc_signals. Just remember that the trace mechanism only observes the value at the end of the evaluation phase; it does not intercept assignments.

The Error Handler Implementation (sysc/utils/sc_report_handler.cpp)

The Accellera report handler uses a set of highly optimized internal maps to route messages. When you invoke SC_REPORT_INFO(), the macro captures __FILE__ and __LINE__ and forwards them to sc_report_handler::report().

The implementation checks the routing rules in this order:

  1. Is there a rule for this specific (Message Type, Severity)?
  2. If not, is there a rule for this specific Severity?
  3. If not, is there a rule for this specific Message Type?
  4. If not, use the default action for that Severity.

If the resulting action contains SC_THROW, the kernel instantiates an sc_report object and throws it as a C++ exception (throw report;). This is why you must never catch generic exceptions (catch (...)) around sc_start(); doing so intercepts SystemC's internal error handling and can lead to corrupted simulation states.

Understanding these mechanisms allows you to write testbenches that gracefully catch specific errors for negative testing (verifying that a model does throw an error on bad input), ensuring your models are both rigorous and compliant.

Comments and Corrections