Chapter 11: Advanced Core Semantics

Kernel Source Map

A guided map through the Accellera SystemC kernel source for modules, processes, events, signals, ports, reports, and TLM.

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.

Kernel Source Map

Reading the Accellera SystemC Proof-of-Concept (PoC) source code is easier when you know which files map to which IEEE 1666 LRM concepts. This page is a map. It is not a replacement for the LRM, but it bridges the gap between theoretical standards and actual C++ implementation.

When a simulator behaves unexpectedly, stepping through these specific files in GDB or Visual Studio is the fastest way to understand the kernel's rules. We will aggressively explore how the Accellera kernel implements these features under the hood.

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.

Core Simulation Context (sc_simcontext)

LRM Reference: Section 4 (Elaboration and Simulation Semantics) Source Files:

  • sysc/kernel/sc_simcontext.h
  • sysc/kernel/sc_simcontext.cpp

The simulation context (sc_simcontext) is the beating heart of SystemC, accessible via the singleton getter sc_get_curr_simcontext().

Under the hood, sc_simcontext maintains several crucial data structures:

  • sc_runnable m_runnable: The run queues for processes ready to execute in the current delta cycle. It holds separated queues for SC_METHODs and SC_THREADs.
  • std::vector<sc_update_action*> m_update_list: Channels that have called request_update() during the evaluation phase are pushed here.
  • sc_pq<sc_event_timed*> m_timed_events: A priority queue of events scheduled for future simulation times.
  • sc_time m_curr_time: The current simulation timestamp (sc_time_stamp()).

The core scheduling algorithm is implemented in sc_simcontext::crunch(). It loops through the m_runnable processes (Evaluation Phase), then processes the m_update_list by invoking virtual update() methods on those primitives (Update Phase), and finally triggers delta notifications, looping until the delta cycle stabilizes.

Modules and Object Hierarchy

LRM Reference: Section 5 (Module and Hierarchy) Source Files:

  • sysc/kernel/sc_object.*
  • sysc/kernel/sc_module.*
  • sysc/kernel/sc_module_name.*
  • sysc/kernel/sc_object_manager.*

Every structural element in SystemC derives from sc_object. Hierarchy is dynamically constructed during C++ instantiation, driven by the sc_module_name class.

When you pass a string to an sc_module constructor, you implicitly instantiate an sc_module_name. Its constructor pushes a pointer to itself onto a static stack inside the sc_object_manager of the simulation context. When the child sc_object (e.g., a port or a sub-module) is constructed inside that scope, it looks at the top of this stack to discover its parent sc_module.

Once the constructor finishes, the sc_module_name destructor pops itself off the stack. This elegant RAII-based scoping avoids the need for users to manually pass parent pointers to every object.

Processes and Scheduling

LRM Reference: Section 5.2 (Processes) Source Files:

  • sysc/kernel/sc_process.*
  • sysc/kernel/sc_method_process.*
  • sysc/kernel/sc_thread_process.*
  • sysc/kernel/sc_coros.* (Coroutines)

A SystemC process is a C++ object. sc_method_process and sc_thread_process inherit from a common sc_process_b base class, which stores the dynamic sensitivity list (events the process is waiting for).

Under the hood, SC_THREADs require actual stack context switching, bypassing the OS thread scheduler. The Accellera PoC uses a coroutine library, traditionally QuickThreads (qt) or standard POSIX threads (pthreads) configured via macros. When you call wait(), the sc_thread_process saves its CPU registers to its allocated stack (via assembly-level context switches like qt_block), and yields control back to sc_simcontext::crunch(), which loads the CPU registers of the next runnable process. SC_METHODs, however, are just standard C++ function pointers executed synchronously, saving memory by not requiring their own call stack.

Events and Time

LRM Reference: Section 5.10 (sc_event) and Section 5.11 (sc_time) Source Files:

  • sysc/kernel/sc_event.*
  • sysc/kernel/sc_time.*

An sc_event is essentially a notification node. Calling notify(SC_ZERO_TIME) adds the event to sc_simcontext::m_delta_events. Calling notify(10, SC_NS) wraps the notification into an sc_event_timed object and inserts it into the m_timed_events priority queue.

Internally, sc_time is represented as an unsigned 64-bit integer (sc_dt::uint64). The absolute integer value represents time accurately scaled against the selected global time resolution (e.g., femtoseconds), preventing floating-point rounding errors across the simulation. The global resolution is stored statically in the sc_time_params struct.

Signals and Primitive Channels

LRM Reference: Section 6 (Predefined Channels) Source Files:

  • sysc/communication/sc_prim_channel.*
  • sysc/communication/sc_signal.*

When a process writes to an sc_signal via operator=(), the signal does not immediately change its value. Instead, sc_signal::write() stores the new value in a temporary m_new_val variable.

If m_new_val differs from m_cur_val, the signal calls request_update(). The base class sc_prim_channel then registers this pointer into the simulation context's m_update_list. Later, during the Update Phase, the kernel traverses m_update_list and calls the pure virtual update() method. The sc_signal's implementation of update() commits m_new_val to m_cur_val and notifies its internal m_value_changed_event.

This delayed assignment ensures determinism and prevents combinational race conditions regardless of the order in which concurrent processes execute.

Complete Example: Accessing Kernel Internals

While you shouldn't rely on implementation-specific APIs, the LRM does guarantee certain kernel introspection APIs. Here is a complete sc_main demonstrates querying the simulation context and object hierarchy.

#include <systemc>
#include <iostream>
#include <vector>
 
SC_MODULE(KernelMapDemo) {
    SC_CTOR(KernelMapDemo) {
        SC_THREAD(run);
    }
    
    void run() {
        wait(10, sc_core::SC_NS);
        
        // LRM API to check simulation status
        std::cout << "[Kernel] Current Time: " << sc_core::sc_time_stamp() << "\n";
        std::cout << "[Kernel] Delta Count: " << sc_core::sc_delta_count() << "\n";
        
        // LRM API to query the object hierarchy
        std::cout << "[Kernel] Module Name: " << this->name() << "\n";
        std::cout << "[Kernel] Object Kind: " << this->kind() << "\n";
        
        sc_core::sc_stop();
    }
};
 
int sc_main(int argc, char* argv[]) {
    KernelMapDemo demo("kernel_demo");
    
    // LRM API to check engine status
    std::cout << "Engine status before start: ";
    if (sc_core::sc_get_status() == sc_core::SC_ELABORATION) {
        std::cout << "ELABORATION\n";
    }
 
    sc_core::sc_start();
    
    std::cout << "Engine status after stop: ";
    if (sc_core::sc_get_status() == sc_core::SC_STOPPED) {
        std::cout << "STOPPED\n";
    }
 
    return 0;
}

Explanation of Execution

Engine status before start: ELABORATION
[Kernel] Current Time: 10 ns
[Kernel] Delta Count: 1
[Kernel] Module Name: kernel_demo
[Kernel] Object Kind: sc_module
Engine status after stop: STOPPED

By tracing through sc_simcontext.cpp, you can see exactly how sc_get_status() updates its internal state machine from SC_ELABORATION -> SC_RUNNING -> SC_STOPPED. Understanding this source map allows you to debug complex deadlock or ordering issues rapidly.

Deep Dive: Accellera Source for sc_signal and update()

The sc_signal<T> channel perfectly illustrates the Evaluate-Update paradigm of SystemC. In the Accellera source (src/sysc/communication/sc_signal.cpp), sc_signal inherits from sc_prim_channel.

The write() Implementation

When you call write(const T&), the signal does not immediately change its value. Instead, it stores the requested value in m_new_val and registers itself with the kernel:

template<class T>
inline void sc_signal<T>::write(const T& value_) {
    if( !(m_new_val == value_) ) {
        m_new_val = value_;
        this->request_update(); // Inherited from sc_prim_channel
    }
}

The request_update() call appends the channel to sc_simcontext::m_update_list.

The update() Phase

After the Evaluate phase finishes (all ready processes have run), the kernel iterates over m_update_list and calls the update() virtual function on each primitive channel. For sc_signal, this looks like:

template<class T>
inline void sc_signal<T>::update() {
    if( !(m_new_val == m_cur_val) ) {
        m_cur_val = m_new_val;
        m_value_changed_event.notify(SC_ZERO_TIME); // Notify processes sensitive to value_changed_event()
    }
}

This guarantees that all concurrent processes see the same old value until the delta cycle advances, perfectly mimicking hardware register delays.

Comments and Corrections