Chapter 5: Source Internals

Simulation Kernel Deep Dive

How evaluate-update-delta scheduling makes concurrent C++ models behave like hardware.

The SystemC scheduler is a discrete-event simulation kernel. Its job is to decide which process runs, when updates become visible, which events wake new work, and when simulation time can advance. This is strictly defined by the IEEE 1666 Language Reference Manual (LRM), primarily in Section 4 on Elaboration and Simulation Semantics.

The Shape of a Simulation Step

According to the LRM, the simulation semantics are broken down into distinct phases: Evaluation, Update, Delta Notification, and Time Advance. We can observe these phases in action by writing a complete model that tracks delta cycles.

#include <systemc>
#include <iostream>
 
using namespace sc_core;
 
SC_MODULE(DeltaTracker) {
  sc_signal<bool> sig_a{"sig_a"};
  sc_signal<bool> sig_b{"sig_b"};
 
  SC_CTOR(DeltaTracker) {
    SC_THREAD(stimulus);
    SC_METHOD(monitor_a);
    sensitive << sig_a;
    dont_initialize();
 
    SC_METHOD(monitor_b);
    sensitive << sig_b;
    dont_initialize();
  }
 
  void stimulus() {
    std::cout << "[Time: " << sc_time_stamp() << ", Delta: " << sc_delta_count() << "] Stimulus: writing sig_a=1\n";
    sig_a.write(true);
    
    wait(SC_ZERO_TIME); // Advance one delta cycle
    
    std::cout << "[Time: " << sc_time_stamp() << ", Delta: " << sc_delta_count() << "] Stimulus: writing sig_b=1\n";
    sig_b.write(true);
    
    wait(10, SC_NS); // Advance time
    
    std::cout << "[Time: " << sc_time_stamp() << ", Delta: " << sc_delta_count() << "] Stimulus: done\n";
  }
 
  void monitor_a() {
    std::cout << "[Time: " << sc_time_stamp() << ", Delta: " << sc_delta_count() << "] monitor_a triggered: sig_a=" << sig_a.read() << "\n";
  }
 
  void monitor_b() {
    std::cout << "[Time: " << sc_time_stamp() << ", Delta: " << sc_delta_count() << "] monitor_b triggered: sig_b=" << sig_b.read() << "\n";
  }
};
 
int sc_main(int argc, char* argv[]) {
  DeltaTracker tracker("tracker");
  sc_start();
  return 0;
}

Evaluate Phase

During the evaluation phase, the kernel selects a ready process and resumes its execution. A ready SC_METHOD runs to completion. A ready SC_THREAD (or SC_CTHREAD) resumes until it calls wait() or returns. The LRM explicitly states that the order of process execution during the evaluation phase is non-deterministic. Your models must not rely on the order in which processes execute within the same delta cycle.

Processes can write to signals, notify events, call TLM transport functions, and update ordinary C++ state. But primitive channel updates (like writes to sc_signal) are deferred.

Update Phase

After all ready processes have been evaluated (the set of runnable processes is empty), the kernel enters the update phase. Every primitive channel that had request_update() called during the evaluate phase now has its update() function executed.

For sc_signal, this is when the new value actually becomes the current value. By deferring the update, SystemC guarantees that all processes reading the signal during the evaluate phase read the old value, regardless of process execution order. This avoids race conditions and mirrors the behavior of hardware registers.

Delta Notification Phase

During the update phase, or directly from processes using zero-delay notification (notify(SC_ZERO_TIME)), events may be scheduled for the next delta cycle.

If there are pending delta notifications, the kernel advances to the next delta cycle (incrementing sc_delta_count()) while keeping the simulation time constant, and moves those notified processes back to the runnable set. It then loops back to the Evaluate phase.

This gives SystemC a way to settle combinational behavior without advancing time.

Time Advance Phase

When the runnable set is empty and there are no more primitive channel updates or delta notifications, the current time step is fully settled. The scheduler then looks for the next earliest timed event (e.g., a process waiting for 10 ns or a delayed notification). Simulation time jumps directly to that time, the relevant events are triggered, processes become ready, and the loop starts again at the Evaluate phase.

Why wait() Needs Process State

SC_THREAD can suspend and resume. That means the implementation needs a process control mechanism (such as coroutines, fibers, or user-level threads like QuickThreads in the reference implementation) that preserves the call stack and execution state across waits. wait() returns control to the kernel, and the kernel context-switches back when the thread resumes.

Debugging With the Kernel Model

When a model behaves strangely, classify the problem based on the LRM phases:

  • Evaluation issue: wrong sensitivity, method ran too early, method initialized unexpectedly.
  • Update issue: signal write not visible until update phase. Did you read immediately after writing in the same process?
  • Delta issue: event notified in a later delta than expected, causing off-by-one delta delays.
  • Time issue: timed wait or clock period mismatch.
  • Termination issue: no timed events remain (simulation ends prematurely), or a process never yields (infinite loop blocking the evaluate phase).

Comments and Corrections