LRM Bridge: Elaboration and Simulation Semantics
The SystemC 1666-2023 model of construction, elaboration, initialization, evaluation, update, delta notification, and timed notification.
How to Read This Lesson
This lesson is an LRM bridge. We translate standard language into the questions you actually ask while debugging and reviewing models.
The IEEE 1666 LRM strictly separates a model's life cycle into elaboration and simulation. This distinction is the key to understanding many API rules and avoiding illegal dynamic structural changes. To fully appreciate this, we must look inside the Accellera SystemC kernel.
Standard and source context
Elaboration
Elaboration is where the structural topology becomes known. Modules are constructed, child objects are named, ports and exports are bound, process macros register behavior, and time resolution is set.
Under the Hood:
The global sc_simcontext singleton maintains the simulation state. During elaboration, creating an sc_module triggers sc_module::sc_module(), which registers the module into sc_simcontext::m_object_manager forming the sc_object hierarchy. Port bindings (port(channel)) push binding requests into a deferred queue (m_bind_info).
You are executing standard C++ constructors, but the SystemC kernel is simultaneously observing and registering the hierarchy. Binding belongs entirely in this phase. Once sc_start() is called, the kernel executes sc_simcontext::elaborate(), iterating over the binding queues to resolve sc_port to sc_interface pointers. Trying to alter topology after this causes an SC_FATAL.
Simulation and the Discrete-Event Scheduler
Simulation begins when control enters the scheduler, normally through sc_core::sc_start(). The LRM describes scheduling through explicit, strictly ordered phases.
In the source code, this is orchestrated by sc_simcontext::initialize() followed by the central sc_simcontext::crunch() and sc_simcontext::next_time() loop.
- Initialization (
initialize()): Processes are made runnable. The kernel iterates over all process handles (sc_method_handle,sc_thread_handle) and pushes them into the runnable queues (m_runnable->push_back()), unlessdont_initialize()was called. - Evaluation (
crunch()part 1): Runnable processes pop from the queue and execute (usingsemantics()for methods, orsuspend_me()/coroutine_resumefor threads) until they yield (wait()) or finish. - Update (
crunch()part 2): Channels commit pending values. The kernel iterates overm_update_list(a vector ofsc_update_if*) and callsupdate()on each channel (e.g.,sc_signal::update()). - Delta Notification (
crunch()part 3): Events with zero-time delay are triggered.m_delta_eventsare processed, which may push new processes intom_runnable. Ifm_runnableis not empty, loop back to Evaluation (Delta Cycle). - Timed Notification (
next_time()): Whenm_runnableandm_delta_eventsare empty,sc_simcontextpeeks atm_timed_events(a standard C++ priority queue sorted by timestamp). It pops the nearest events, updatesm_curr_time, schedules the associated processes intom_runnable, and loops back tocrunch().
End-to-End LRM Example
Here is a complete example demonstrates elaboration, initialization, delta cycles, and timed notification, strictly following IEEE 1666 semantics.
#include <systemc>
// A module demonstrating initialization and evaluation phases
SC_MODULE(SemanticsDemo) {
sc_core::sc_signal<bool> ready{"ready"};
sc_core::sc_event timed_event;
SC_CTOR(SemanticsDemo) {
// Registered during elaboration
SC_METHOD(initialization_method);
// By default, methods are placed in the runnable queue during initialization.
// We do not call dont_initialize() here.
SC_THREAD(simulation_thread);
// Wait for the ready signal to go true
sensitive << ready.pos();
}
// This method runs automatically at time 0 (Initialization Phase)
void initialization_method() {
std::cout << "@" << sc_core::sc_time_stamp()
<< " [Init/Eval Phase] Method executing." << std::endl;
// Write a value. This schedules an Update request, it does NOT change the value instantly.
ready.write(true);
// Schedule a timed event for 10 ns in the future
timed_event.notify(10, sc_core::SC_NS);
// Prevent it from re-running endlessly
next_trigger(timed_event);
}
void simulation_thread() {
while(true) {
// Wakes up when ready becomes true (which happens in the Delta Update phase)
wait();
std::cout << "@" << sc_core::sc_time_stamp()
<< " [Eval Phase] Thread woke up from ready.pos() delta event." << std::endl;
// Suspend until the timed event fires
wait(timed_event);
std::cout << "@" << sc_core::sc_time_stamp()
<< " [Timed Notification] Thread woke up from timed_event." << std::endl;
}
}
};
int sc_main(int argc, char* argv[]) {
// Elaboration Phase
SemanticsDemo demo("demo");
std::cout << "--- Starting Simulation ---" << std::endl;
// Simulation Phase begins
sc_core::sc_start(20, sc_core::SC_NS);
std::cout << "--- Simulation Finished ---" << std::endl;
return 0;
}Initialization Rules
Before time advances, some processes are placed in the runnable queue. That is why SC_METHOD processes often run at time zero. Under the hood, dont_initialize() sets a boolean flag m_dont_init in the sc_process_b base class, which sc_simcontext::initialize() reads to skip pushing it into m_runnable.
For clocked behaviors, you almost always want to prevent this initial time-zero execution:
SC_METHOD(tick);
sensitive << clk.pos();
dont_initialize(); // Crucial to prevent time-zero glitchingWithout dont_initialize(), your "clocked" behavior will execute before the very first clock edge actually occurs.
Delta Cycles vs Timed Notification
Delta cycles let the model settle without advancing physical simulation time. A delta cycle can propagate events from one process to another while sc_time_stamp() remains strictly unchanged. This is not an implementation trick; it is a fundamental pillar of hardware description languages to model concurrency on parallel wires.
Timed Notification occurs only when the delta-event queue is empty. The scheduler jumps time forward to the nearest pending timed event in the m_timed_events priority queue.
Practical Questions to Ask
When debugging LRM-compliant models:
- Is this code executing during a constructor (elaboration) or during
sc_start(simulation)? - Did the process unintentionally initialize at time zero because
dont_initialize()was forgotten? - Is the read value pending an update (in
m_update_list), or has the update phase already committed it? - Did the event notify immediately (
notify()), in a delta (notify(SC_ZERO_TIME)), or at a future time (notify(time))?
Source-reading checkpoint
When elaboration order looks surprising, inspect the Accellera sc_module and sc_simcontext paths. That is where constructed objects become kernel-visible hierarchy before simulation starts. For binding-related elaboration, continue into sc_port_registry.
Can you answer these clearly?
Keep moving when you can answer each question without looking back at the lesson.
Comments and Corrections