Chapter 6: Practice

Testing, Tracing, and Debugging SystemC Models

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

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.

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.

Comments and Corrections