Simulation Phases in SystemC — From sc_main() to Cleanup
Every SystemC simulation follows the same lifecycle. Modules are created, ports are bound, processes are registered, and only then does the kernel take over and start executing. Understanding exactly when each phase starts, what you can do in it, and what is forbidden is the difference between a simulation that works and one that crashes mysteriously at startup.
This article walks through every phase from the first line of sc_main() to the final destructor call — including the four callback hooks your modules can override to execute code at phase boundaries.
The Big Picture
A SystemC simulation has four distinct phases:
Each phase has hard rules about what the kernel allows. Violating them — like writing to a signal during elaboration or binding a port after sc_start() — results in runtime errors or undefined behavior.
Phase 1: Elaboration
Elaboration is everything that happens before sc_start() is called. This is pure C++ — the kernel is not running yet. You are simply constructing a hierarchy of C++ objects.
int sc_main(int argc, char* argv[]) { // ── Elaboration begins here ── sc_clock clk("clk", 10, SC_NS); // create clock signal sc_signal<bool> reset("reset"); // create signal MyModule dut("dut"); // instantiate module (runs constructor) dut.clk(clk); // bind port dut.reset(reset); // bind port // ── Elaboration ends, execution begins ── sc_start(100, SC_NS); // hand control to the kernel return 0; }
During elaboration, three things happen inside every module constructor:
- Ports and signals are created — declared as member objects, constructed by C++
- Processes are registered —
SC_THREADandSC_METHODcalls register process functions with the kernel and attach static sensitivity lists - Sub-modules are instantiated — hierarchical designs build the module tree recursively through constructor calls
sig.write() or event.notify() during elaboration. The kernel's delta-cycle machinery is not running. Signal values are just C++ objects — writes will not propagate or trigger any process.
Module Constructor Example
struct Counter : sc_module { sc_in<bool> clk; sc_out<int> count; SC_CTOR(Counter) { // registering a process — elaboration work SC_THREAD(run); sensitive << clk.pos(); // count.write(0); ← WRONG: kernel not running yet } void run() { count.write(0); // correct: done in process body while(true) { wait(); count.write(count.read() + 1); } } };
Phase 2: Initialization — The First Delta
When sc_start() is called, the kernel runs a special initialization pass before advancing simulation time. Every registered process (both SC_THREAD and SC_METHOD) is invoked once in an unspecified but deterministic order.
For SC_THREAD, this means execution starts from the top of the function and runs until it hits the first wait(). For SC_METHOD, the function body runs once completely.
void run() { // SC_THREAD count.write(0); // ← runs during initialization wait(); // ← suspends here, waits for sensitivity while(true) { count.write(count.read() + 1); wait(); } }
This initialization pass is why most SC_THREAD processes begin with a single write (to reset outputs to a known state) before the first wait(). It is your chance to set up initial signal values before any clock edge fires.
dont_initialize() after registering a process, that process is skipped during the initialization pass. It will only run when one of its sensitivity events fires. Use this when a process must not assume any initial state and should only react to real events. For the full guide on events and sensitivity lists, see Events and Sensitivity in SystemC.
SC_METHOD(check); sensitive << clk.pos(); dont_initialize(); // skip the initialization run for this process
Phase 3: The Execution Loop
After initialization, the kernel enters the main event-driven simulation loop — the evaluate-update cycle covered in detail in the delta cycles article and the sc_signal article.
In brief, at each simulation timestep:
- Evaluate — all runnable processes execute; signal writes are buffered
- Update — buffered values are committed; changed signals fire events
- If new events fired, another delta cycle begins at the same timestamp
- When no more processes are ready, time advances to the next scheduled event
- When no events remain and time has expired (or
sc_stop()was called), execution ends
sc_stop() inside a process requests the kernel to stop after the current delta cycle finishes. sc_start() then returns in sc_main(). You can call sc_start() again to resume — it picks up from where it stopped.
Phase 4: End-of-Simulation and Cleanup
When sc_start() returns, the kernel has stopped. You are back in plain C++ in sc_main(). At this point:
- The simulation time is frozen — no more delta cycles will run
- You can read signal values (they won't change)
- You can print reports, dump statistics, check final state
- When
sc_main()returns, module destructors run in reverse construction order
sc_start(1000, SC_NS); // ── back in sc_main, kernel stopped ── cout << "Final count: " << dut.count.read() << endl; cout << "Sim time: " << sc_time_stamp() << endl; return 0; // destructors run here
The Four Lifecycle Callbacks
Every sc_module can override four virtual functions that the kernel calls at precise phase boundaries. They fire for every module instance in the design — not just once globally.
Code Example
struct Top : sc_module { SC_CTOR(Top) { /* instantiate sub-modules, bind ports */ } void before_end_of_elaboration() override { // still in elaboration — can add dynamic ports or sub-modules sc_report_handler::stop_after(SC_ERROR, 10); } void end_of_elaboration() override { // all ports are bound — safe to inspect hierarchy static bool once = false; if (!once) { once = true; cout << "Netlist ready, instance count: " << ++instance_count << endl; } } void start_of_simulation() override { // just before init pass — good for opening trace files tf = sc_create_vcd_trace_file("waves"); sc_trace(tf, clk, "clk"); } void end_of_simulation() override { // after sc_start() returns — final reports static bool once = false; if (!once) { once = true; sc_close_vcd_trace_file(tf); cout << "Errors seen: " << error_count << endl; } } };
end_of_simulation() runs 10 times. Wrap singleton logic (like closing a file or printing a summary) in a static bool once guard as shown above.
Callback vs Constructor: What Goes Where
| Task | Where to put it |
|---|---|
| Create signals, ports, sub-modules | Constructor |
| Register SC_THREAD / SC_METHOD | Constructor |
| Add dynamic structure (late binding) | before_end_of_elaboration() |
| Inspect final netlist / port connections | end_of_elaboration() |
| Set initial signal values | Process body (before first wait()) |
| Open trace files, read config | start_of_simulation() |
| Close files, print coverage, check assertions | end_of_simulation() |
Common Mistakes
Writing signals in the constructor
The most common elaboration mistake. The kernel hasn't started — no delta cycle will process the write, and the signal's initial value is simply whatever the constructor set (usually zero-initialized). The write silently does nothing useful.
SC_CTOR(Dut) { reset.write(true); // ← pointless, kernel not running SC_THREAD(run); sensitive << clk.pos(); }
Binding ports after sc_start()
Port binding must be complete before sc_start() is called. Attempting to bind a port during simulation (e.g., inside a process) causes a kernel error. All structural decisions are locked in at the end of elaboration.
Using sc_time_stamp() during elaboration
sc_time_stamp() returns 0 ns during elaboration and initialization. This is expected — the clock hasn't ticked. Don't use it for logic that needs a meaningful time value until the execution loop is running.
Assuming callback order across modules
While callbacks within a module follow a strict order (before_end_of_elaboration → end_of_elaboration → start_of_simulation → end_of_simulation), the order across different module instances is unspecified. Don't write a callback in module A that depends on a callback in module B having already run.
Complete Phase Timeline
| Phase | What happens | Callbacks fired |
|---|---|---|
| Elaboration | Constructors run, ports bound, processes registered | before_end_of_elaboration()end_of_elaboration() |
| Pre-simulation | Kernel validates connectivity, prepares scheduler | start_of_simulation() |
| Initialization | All processes run once (unless dont_initialize()) |
— |
| Execution | Evaluate-update loop, time advances | — |
| End of simulation | sc_start() returns, kernel stopped |
end_of_simulation() |
| Cleanup | Destructors run in reverse construction order | — |
Summary
- Elaboration is pure C++ — constructors run, ports bind, processes register. The kernel is not active.
- Initialization runs every process once when
sc_start()is first called. Use it to set initial output values. - Execution is the event-driven evaluate-update loop. Time advances when no processes are ready.
- End-of-simulation fires after
sc_start()returns. Use it for reports and file cleanup. - The four callbacks —
before_end_of_elaboration(),end_of_elaboration(),start_of_simulation(),end_of_simulation()— fire for every module instance. Usestatic bool oncefor singleton logic. - Never write signals in a constructor, bind ports after
sc_start(), or assume callback ordering across modules.