Chapter 7: SystemC 1666-2023 LRM

LRM Bridge: Elaboration and Simulation Semantics

The SystemC 1666-2023 model of construction, elaboration, initialization, evaluation, update, delta notification, and timed notification.

Listen to this lessonAudiobook mode

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.

  1. 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()), unless dont_initialize() was called.
  2. Evaluation (crunch() part 1): Runnable processes pop from the queue and execute (using semantics() for methods, or suspend_me()/coroutine_resume for threads) until they yield (wait()) or finish.
  3. Update (crunch() part 2): Channels commit pending values. The kernel iterates over m_update_list (a vector of sc_update_if*) and calls update() on each channel (e.g., sc_signal::update()).
  4. Delta Notification (crunch() part 3): Events with zero-time delay are triggered. m_delta_events are processed, which may push new processes into m_runnable. If m_runnable is not empty, loop back to Evaluation (Delta Cycle).
  5. Timed Notification (next_time()): When m_runnable and m_delta_events are empty, sc_simcontext peeks at m_timed_events (a standard C++ priority queue sorted by timestamp). It pops the nearest events, updates m_curr_time, schedules the associated processes into m_runnable, and loops back to crunch().

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 glitching

Without 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.

Lesson self-check

Can you answer these clearly?

Keep moving when you can answer each question without looking back at the lesson.

Comments and Corrections