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:
- Which model object failed?
- What simulated time was it?
- What operation was attempted?
- 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