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:

Elaboration Modules created, ports bound, processes registered before sc_start()
Initialization All processes run once in unspecified order first delta @ t=0
Execution Event-driven evaluate–update loop sc_start() running
Cleanup Destructors called, reports generated after sc_start() returns

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:

  1. Ports and signals are created — declared as member objects, constructed by C++
  2. Processes are registeredSC_THREAD and SC_METHOD calls register process functions with the kernel and attach static sensitivity lists
  3. Sub-modules are instantiated — hierarchical designs build the module tree recursively through constructor calls
Forbidden during elaboration Never call 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() If you call 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:

  1. Evaluate — all runnable processes execute; signal writes are buffered
  2. Update — buffered values are committed; changed signals fire events
  3. If new events fired, another delta cycle begins at the same timestamp
  4. When no more processes are ready, time advances to the next scheduled event
  5. When no events remain and time has expired (or sc_stop() was called), execution ends
sc_stop() vs sc_start() returning Calling 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:

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.

Callback When it fires
before_end_of_elaboration() After all constructors, before port binding is finalized. Can still add dynamic structure here.
end_of_elaboration() After all ports are bound and hierarchy is complete. Good for netlist inspection and connectivity checks.
start_of_simulation() Just before the initialization pass. Good for channel/port initialization and opening trace files.
end_of_simulation() After sc_start() returns. Good for closing files, printing statistics, checking coverage.

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;
    }
  }
};
Static guard pattern Each callback fires once per module instance — if you have 10 instances of a module, 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_elaborationend_of_elaborationstart_of_simulationend_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


📬 Get new articles in your inbox

Deep dives on SystemC, C++, and embedded systems — no spam, unsubscribe any time.

Aditya Gaurav

Aditya Gaurav

Embedded systems engineer specializing in SystemC, ARM architecture, and C/C++ internals. Writing deep technical dives for VLSI and embedded engineers.