The Execution Phases
A deep dive into the SystemC execution model: Elaboration, Initialization, Evaluation, Update, and advancing time.
How to Read This Lesson
These core semantics are where experienced SystemC engineers earn their calm. We will name the scheduler rule, then show how the source enforces it.
The Execution Phases
The SystemC Language Reference Manual (IEEE 1666) defines a strict execution model that governs how your C++ code behaves like parallel hardware. Understanding these phases is the key to mastering SystemC.
Under the Hood: C++ Implementation in Accellera SystemC
To understand how a sequential C++ program simulates parallel hardware, look at the core of the Accellera systemc repository (src/sysc/kernel/sc_simcontext.cpp):
sc_simcontext: This is the heart of the SystemC kernel. It is a global singleton object that manages the simulation time, the hierarchy tree (sc_object_manager), and the event queues.- The Runnable Queue (
sc_runnable): When an event triggers, thesc_simcontextpushes the sensitivesc_process_b(the base class for threads and methods) onto thesc_runnablequeue. The evaluation phase is literally awhile (!runnable->is_empty())loop that pops processes and executes them. - Context Switching: How does
sc_core::wait()work? Accellera SystemC uses user-level coroutines (historically QuickThreads, now often ucontext or POSIX threads depending on the OS). When a thread callswait(), the C++ execution context (registers, stack pointer) is saved, and control yields back to thesc_simcontextevaluation loop. - The Update Queue: Primitive channels (like
sc_signal) inherit fromsc_prim_channel. When you write a new value, the channel callsrequest_update(). This simply pushes a pointer to the channel onto them_update_listarray insidesc_simcontext. During the update phase, the kernel loops over this array and calls the virtualupdate()method on each channel.
Source and LRM Trail
Advanced core behavior should always be checked against Docs/LRMs/SystemC_LRM_1666-2023.pdf before source details. For implementation, read .codex-src/systemc/src/sysc/kernel and .codex-src/systemc/src/sysc/communication, especially the scheduler, events, object hierarchy, writer policy, report handler, and async update path.
1. Elaboration Phase
Before the simulation clock even starts ticking, SystemC builds the hierarchy.
- Module Instantiation: The constructors (
SC_CTORor custom constructors) of allsc_modulederived classes are executed. - Port Binding: Ports (
sc_in,sc_out,sc_port) are bound to channels or interfaces. - Process Registration:
SC_METHOD,SC_THREAD, andSC_CTHREADare registered with the kernel.
[!WARNING] You cannot bind ports after the elaboration phase has completed. Once
sc_start()is called, the hierarchy is locked.
2. Initialization Phase
When sc_start() is invoked, the kernel enters initialization:
- Every registered
SC_METHODandSC_THREADis executed exactly once, unlessdont_initialize()was explicitly called on it during elaboration. - The processes run until they yield (e.g., hit a
wait()) or return.
3. The Evaluation Phase
This is the core of the simulation loop.
- The scheduler selects a runnable process from the runnable queue.
- The process executes. If it writes to a signal (
sc_signal::write), the new value is not immediately visible. Instead, the signal is added to the update queue. - The evaluation phase continues until the runnable queue is entirely empty.
4. The Update Phase
Once all processes have suspended, the kernel processes the update queue.
- All primitive channels (like
sc_signal) apply their pending writes. The "next" value becomes the "current" value. - If the value changed, any process sensitive to that channel's events is pushed back onto the runnable queue.
5. Delta Cycles
If the update phase caused new processes to become runnable, the kernel returns to the Evaluation Phase without advancing the simulation time. This loop (Evaluate -> Update -> Evaluate) is called a Delta Cycle. It allows zero-delay combinational logic to settle deterministically.
6. Advancing Time
If the runnable queue and update queue are both empty, the kernel looks at the event queue for pending timed events (e.g., wait(10, SC_NS)).
- The simulation time is advanced to the timestamp of the earliest pending event.
- The processes waiting on that event are moved to the runnable queue.
- The cycle repeats.
Complete Execution Example
Below is a complete, compilable sc_main example that demonstrates the elaboration, initialization, evaluation, and update phases in action.
#include <systemc>
#include <iostream>
SC_MODULE(phase_demo) {
sc_core::sc_signal<int> sig;
SC_CTOR(phase_demo) : sig("sig") {
std::cout << "1. Elaboration Phase: Constructor running." << std::endl;
SC_THREAD(stimulus_thread);
// We do NOT use dont_initialize() here, so it runs during initialization.
SC_METHOD(monitor_method);
sensitive << sig;
dont_initialize(); // Only run when 'sig' actually changes
}
void stimulus_thread() {
std::cout << "2. Initialization Phase: Thread running for the first time." << std::endl;
// Wait for some time to pass
sc_core::wait(10, sc_core::SC_NS);
std::cout << "3. Evaluation Phase (@" << sc_core::sc_time_stamp()
<< "): Thread writing to signal." << std::endl;
// This schedules an update, but does not change the value immediately
sig.write(42);
std::cout << " Immediate read after write (old value): " << sig.read() << std::endl;
// Yield to allow the update phase and subsequent delta cycle to run
sc_core::wait(sc_core::SC_ZERO_TIME);
std::cout << "6. Evaluation Phase (Next Delta): Value is now updated to: "
<< sig.read() << std::endl;
}
void monitor_method() {
std::cout << "5. Evaluation Phase (Delta Cycle): Monitor triggered! Signal value is: "
<< sig.read() << std::endl;
}
};
int sc_main(int argc, char* argv[]) {
std::cout << "Starting Elaboration..." << std::endl;
phase_demo demo("demo");
std::cout << "Starting Simulation..." << std::endl;
sc_core::sc_start();
return 0;
}Running this program clearly illustrates how the SystemC kernel defers signal updates until the evaluation phase completes, ensuring deterministic hardware-like behavior in a sequential software environment.
Standard and Source Deep Dive: Port Binding
Port binding is the topological glue of a SystemC model. The IEEE 1666-2023 LRM Sections 4.2.1 (Elaboration) and Section 6.11-6.13 (Ports, Exports, Interfaces) rigidly define how structural connections are made and verified.
Inside the Accellera Source: sc_port_b and sc_port_registry
In src/sysc/communication/sc_port.h/cpp, all specialized sc_port<IF> classes derive from a non-template base class sc_port_b.
When you declare sc_port<BusIf> bus{"bus"};, the constructor ultimately calls sc_simcontext::get_port_registry()->insert(this).
The sc_port_registry (located in src/sysc/kernel/sc_simcontext.cpp) is the global list of every port in the simulation.
When you write cpu.bus.bind(subsystem.target); in your C++ code, you are invoking the bind() method on sc_port. However, this does not immediately resolve the C++ pointer! Instead, the port simply stores a generic pointer to the bound object in an internal array (because a port can be bound to multiple channels if the port's N parameter is > 1).
The Elaboration Phase: complete_binding()
The real magic happens when sc_start() is called.
Before simulation begins, sc_start() invokes sc_simcontext::elaborate(), which ultimately calls sc_port_registry::complete_binding().
If you trace sysc/kernel/sc_simcontext.cpp, you will see complete_binding() iterate over every single port in the design. For each port:
- It traverses the binding tree. If Port A is bound to Port B, and Port B is bound to Channel C, it recursively walks from A -> B -> C to find the actual
sc_interfaceimplementation. - Type Checking: It uses C++ RTTI (
dynamic_cast) to verify that the target object actually implements the interface required by the port.// Abstract representation of the kernel's check: sc_interface* target_if = dynamic_cast<sc_interface*>(bound_object); if (!target_if) { SC_REPORT_ERROR("Port binding failed: interface mismatch"); } - It resolves the final interface pointer and stores it directly inside the port's
m_interfacepointer array.
Zero-Overhead Simulation Dispatch
Why delay pointer resolution until complete_binding()? Because once elaboration finishes, the port has an absolute, direct C++ pointer to the implementing channel.
In src/sysc/communication/sc_port.h, the overloaded operator-> is extraordinarily simple:
template <class IF>
inline IF* sc_port<IF>::operator -> () {
return m_interface;
}During simulation, when a thread executes bus->write(0x10, data);, there are no map lookups, no string comparisons, and no routing tables. It is exactly equivalent to a direct C++ virtual function call on the channel object.
Comments and Corrections