sc_main Deep Dive — SystemC Simulation Entry Point
Every SystemC simulation has exactly one entry point: sc_main. It's the function where you build the design hierarchy, wire up ports, configure the simulation, and hand control to the kernel. If SC_MODULE is where you describe what a component does, sc_main is where you describe how everything connects and when it runs.
This article covers everything that happens in sc_main — the elaboration phase, sc_start() semantics, how to control simulation time, VCD waveform tracing, error handling with sc_report_handler, and how to inspect state after simulation ends.
1. The Role of sc_main
sc_main replaces the standard C++ main(). The SystemC library provides its own main() that calls sc_main after performing kernel initialization. You never write int main() in a SystemC project.
int sc_main(int argc, char* argv[]) { // 1. Elaborate — instantiate modules, bind ports // 2. Configure — set up tracing, initial signal values // 3. Simulate — call sc_start() // 4. Analyze — read results, check assertions return 0; }
The return value works like a normal C++ return: 0 means success. Non-zero values are passed back to the calling shell, useful for CI pass/fail detection.
main() performs this setup, then calls your sc_main. This is why SystemC executables link against -lsystemc — the library supplies main().
2. Elaboration — The Construction Phase
Elaboration is everything that happens in sc_main before sc_start() is called. During elaboration:
- Module constructors run (
SC_CTORbodies execute) - Processes are registered with the kernel (
SC_THREAD,SC_METHOD) - Ports are bound to signals
- Sensitivity lists are finalized
- The simulation hierarchy is fixed — you cannot add or remove modules after
sc_start()
Typical elaboration structure
int sc_main(int, char**) { // Signals (channels between modules) sc_signal<bool> reset; sc_signal<int> data_bus; sc_clock clk("clk", sc_time(10, SC_NS)); // Module instantiation CPU cpu("cpu"); RAM ram("ram"); DMA dma("dma"); // Port binding — connect modules to signals cpu.clk(clk); cpu.reset(reset); cpu.data(data_bus); ram.clk(clk); ram.data(data_bus); dma.clk(clk); dma.data(data_bus); // Initial conditions reset.write(true); // Start simulation sc_start(1000, SC_NS); return 0; }
sc_main — not inside helper functions that return before simulation ends.
3. sc_start() — Running the Simulation
sc_start() transfers control from your code to the SystemC kernel. The kernel runs the simulation until one of its stopping conditions is met, then returns control back to sc_main.
sc_start() variants
| Call | Behaviour |
|---|---|
sc_start() |
Run until no more events are scheduled (run to completion) |
sc_start(100, SC_NS) |
Advance simulation by exactly 100 ns, then return |
sc_start(sc_time(500, SC_NS)) |
Same as above using an sc_time object |
sc_start(SC_ZERO_TIME) |
Run one delta cycle (all pending evaluate-update passes at current time) |
You can call sc_start() multiple times. Control returns to sc_main between calls, letting you inspect signal state, modify inputs, or conditionally decide how much more time to simulate:
// Reset for 2 clock cycles reset.write(true); sc_start(20, SC_NS); // Release reset and run main test reset.write(false); sc_start(500, SC_NS); // Inject a fault mid-simulation data_bus.write(0xDEAD); sc_start(100, SC_NS); // Check final state cout << "Status: " << status_reg.read() << endl;
sc_start() calls, the write takes effect at the start of the next sc_start() — specifically during the first delta cycle. This is correct and expected; you're writing during elaboration of the next simulation segment.
4. sc_stop() — Stopping from Inside a Process
While sc_start(N, SC_NS) stops after N nanoseconds of simulation time, sometimes you need to stop based on a condition discovered inside a process — for example, when a testbench detects a failure or the design signals completion.
void monitor_proc() { while (true) { wait(clk.posedge_event()); if (error_flag.read()) { cout << "[FAIL] Error at " << sc_time_stamp() << endl; sc_stop(); // signals the kernel to stop after this delta } if (done_flag.read()) { cout << "[PASS] Simulation complete at " << sc_time_stamp() << endl; sc_stop(); } } }
sc_stop() does not immediately halt the simulation. It sets a flag in the kernel that causes sc_start() to return at the end of the current delta cycle. All processes currently in their evaluate phase complete normally first.
| Function | Where called | Effect |
|---|---|---|
sc_start(N, SC_NS) |
sc_main |
Advance N ns, then return to sc_main |
sc_stop() |
Any process or sc_main |
Request kernel to stop; sc_start() returns at end of current delta |
sc_pause() |
Any process or sc_main |
Pause simulation; can resume with another sc_start() |
5. Simulation Time — sc_time_stamp() and sc_time_remaining()
Two functions let you inspect the simulation clock from anywhere — including inside processes:
// Current simulation time sc_time now = sc_time_stamp(); cout << "Time: " << now << endl; // prints e.g. "100 ns" cout << now.to_double() << endl; // double in current time unit cout << now.to_string() << endl; // "100 ns" // Time remaining in current sc_start() budget // (only valid inside an sc_start() call) sc_time remaining = sc_time_remaining();
Time resolution
SystemC maintains a global time resolution — the smallest representable time unit. The default is 1 ps. You can change it before the first sc_start() call (and before any signal construction):
// Set resolution to 1 fs — must come before anything else in sc_main sc_set_time_resolution(1, SC_FS); // Default: 1 ps // Once set (or used), resolution is locked — cannot change again
sc_time objects or signals. The moment any sc_time is constructed, the resolution locks. Attempting to change it afterward throws a runtime error.
6. VCD Waveform Tracing
Value Change Dump (VCD) is the standard format for recording signal waveforms. GTKWave, ModelSim, and Verdi can all open VCD files. SystemC has built-in VCD support:
int sc_main(int, char**) { sc_clock clk("clk", sc_time(10, SC_NS)); sc_signal<bool> reset; sc_signal<int> data; MyModule dut("dut"); dut.clk(clk); dut.reset(reset); dut.data(data); // Open VCD file — must be before sc_start() sc_trace_file* tf = sc_create_vcd_trace_file("sim_waves"); tf->set_time_unit(1, SC_NS); // waveform time unit // Register signals to trace sc_trace(tf, clk, "clk"); sc_trace(tf, reset, "reset"); sc_trace(tf, data, "data"); // Run simulation reset.write(true); sc_start(20, SC_NS); reset.write(false); sc_start(200, SC_NS); // Close trace — flushes and finalizes the VCD file sc_close_vcd_trace_file(tf); return 0; // Output: sim_waves.vcd }
The traced signals appear in sim_waves.vcd. Open it with gtkwave sim_waves.vcd to see the full timing diagram.
What can be traced?
| Type | sc_trace() works? |
|---|---|
sc_signal<bool>, sc_clock | Yes — standard |
sc_signal<int>, sc_signal<double> | Yes |
sc_signal<sc_uint<N>>, sc_lv<N> | Yes |
Plain C++ variables (int x) | Yes — but updates only captured at signal change boundaries |
| Custom structs | Only if you implement the sc_trace overload for them |
7. Error Handling with sc_report_handler
SystemC uses a unified reporting system for all runtime messages — warnings, errors, and fatal conditions. By default, errors print to stderr and abort the simulation. You can intercept and customize this behavior with sc_report_handler.
Default severity levels
| Macro | Default action |
|---|---|
SC_REPORT_INFO("id", "msg") | Print to stdout |
SC_REPORT_WARNING("id", "msg") | Print warning to stderr |
SC_REPORT_ERROR("id", "msg") | Print error, throw sc_exception |
SC_REPORT_FATAL("id", "msg") | Print fatal, call abort() |
Custom error handler
void my_handler(const sc_report& rep, const sc_actions& actions) { if (rep.get_severity() == SC_ERROR) { cerr << "[ERR] " << rep.get_msg() << " @ " << sc_time_stamp() << endl; sc_stop(); // stop simulation on any error } else { sc_report_handler::default_handler(rep, actions); } } int sc_main(int, char**) { sc_report_handler::set_handler(my_handler); // ... rest of sc_main }
Setting actions per severity
// Make warnings throw an exception (fail fast in CI) sc_report_handler::set_actions(SC_WARNING, SC_THROW); // Silence SC_INFO messages entirely sc_report_handler::set_actions(SC_INFO, SC_DO_NOTHING); // Log all messages to a file sc_report_handler::set_log_file_name("sim.log");
8. Post-Simulation Analysis
After sc_start() returns, the simulation has ended but signal objects are still alive. You can read final signal values, compute coverage metrics, and print pass/fail results:
sc_start(1000, SC_NS); // Read final state cout << "Final PC: " << pc.read() << endl; cout << "Final status: " << status.read() << endl; cout << "Sim ended at: " << sc_time_stamp() << endl; // Assert expected values if (result.read() != EXPECTED) { cerr << "FAIL: expected " << EXPECTED << " got " << result.read() << endl; return 1; // non-zero = CI failure } cout << "PASS" << endl; return 0;
You cannot call sc_start() again after sc_stop() has been called — the simulation is finalized. For interactive or multi-run scenarios, use sc_pause() instead.
9. Command-Line Arguments
sc_main(int argc, char* argv[]) receives the same command-line arguments as a regular C++ main(). Use them to parameterize simulation runs:
int sc_main(int argc, char* argv[]) { int sim_ns = 1000; // default duration if (argc > 1) sim_ns = atoi(argv[1]); // ... build design ... sc_start(sim_ns, SC_NS); return 0; } // Run: ./sim 5000 → simulates 5000 ns // Run: ./sim → simulates 1000 ns (default)
10. Complete sc_main Template
Here is a production-ready sc_main template you can use as the starting point for any SystemC project:
#include <systemc.h> #include "my_module.h" int sc_main(int argc, char* argv[]) { // ── Time resolution (before anything else) ────────────────── sc_set_time_resolution(1, SC_PS); // ── Error handling ─────────────────────────────────────────── sc_report_handler::set_actions(SC_ERROR, SC_DISPLAY | SC_STOP); sc_report_handler::set_actions(SC_WARNING, SC_DISPLAY); // ── Clocks and signals ─────────────────────────────────────── sc_clock clk ("clk", sc_time(10, SC_NS)); sc_signal<bool> reset; sc_signal<int> data_out; // ── DUT instantiation + port binding ───────────────────────── MyModule dut("dut"); dut.clk(clk); dut.reset(reset); dut.data_out(data_out); // ── VCD tracing ────────────────────────────────────────────── sc_trace_file* tf = sc_create_vcd_trace_file("waves"); sc_trace(tf, clk, "clk"); sc_trace(tf, reset, "reset"); sc_trace(tf, data_out, "data_out"); // ── Simulation sequence ────────────────────────────────────── reset.write(true); sc_start(20, SC_NS); // hold reset 2 cycles reset.write(false); sc_start(980, SC_NS); // main test run // ── Post-sim analysis ──────────────────────────────────────── sc_close_vcd_trace_file(tf); bool pass = (data_out.read() == 42); cout << (pass ? "PASS" : "FAIL") << " @ " << sc_time_stamp() << endl; return pass ? 0 : 1; }
Summary
sc_mainis the SystemC entry point — the library providesmain()which calls yoursc_mainafter kernel init.- Elaboration happens before
sc_start(): modules are instantiated, ports bound, sensitivity lists registered. sc_start(N, SC_NS)runs the simulation for N nanoseconds and returns. You can call it multiple times to step through scenarios.sc_stop()signals the kernel to end the currentsc_start()— safe to call from inside any process.sc_time_stamp()returns the current simulation time — readable from anywhere including inside processes.- Set time resolution with
sc_set_time_resolution()before anysc_timeobjects are created. - VCD tracing: open with
sc_create_vcd_trace_file(), register signals withsc_trace(), close withsc_close_vcd_trace_file(). - Use
sc_report_handlerto customize error handling — especially useful for CI: return non-zero fromsc_mainon test failure. - For detailed coverage of
sc_clock— which is instantiated insc_main— see sc_clock in SystemC.
Part of the SystemC Foundations guide Browse all SystemC guides →