Chapter 11: Advanced Core Semantics

Understanding Delta Cycles

Why Delta Cycles exist, how they solve non-determinism, and how to debug zero-delay loops.

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.

Understanding Delta Cycles

A Delta Cycle is the cornerstone of SystemC's ability to model parallel hardware using a sequential software language (C++).

Under the Hood: C++ Implementation in Accellera SystemC

To understand delta cycles, we must look at how the sc_simcontext scheduler evaluates time. In the Accellera SystemC repository, the scheduler has two primary time concepts: physical time (sc_time) and delta count (an integer).

When a process calls sc_core::wait(sc_core::SC_ZERO_TIME), the kernel does not put that process into the standard timed event queue. Instead, it places the process into a special "delta event queue". After the current update phase completes, the kernel checks this delta queue. If it is not empty, it increments its internal m_delta_count variable, moves the processes from the delta queue to the runnable queue, and loops again without modifying the physical m_curr_time variable.

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.

The Non-Determinism Problem

Imagine two logic gates connected in series, executing in C++. If they are modeled as simple sequential assignments, the order of execution matters.

If Process A runs first and writes to a signal, and Process B runs second and reads it, the result is deterministic. But if the OS scheduler happens to run Process B first, Process B reads the old value of the signal. Real hardware evaluates simultaneously. Software evaluates sequentially, creating a race condition.

The Delta Cycle Solution

SystemC solves this via the Evaluate-Update paradigm as mandated by the IEEE 1666 standard.

  1. Evaluate: Both Process A and Process B run in an arbitrary order. Process A calls Signal1.write(1). SystemC does not change the value of Signal1. Instead, it schedules the change in the update queue.
  2. Update: Once all runnable processes finish and suspend, the kernel applies the writes in the update queue. Signal1 officially becomes 1.
  3. Delta: Because Signal1 changed, Process B (which is sensitive to it) wakes up. Time has not advanced. We are at T=0 + 1 delta cycle. Process B evaluates again, this time seeing the deterministic, updated value.

Complete Delta Cycle Example

Below is a complete, fully compilable sc_main program that demonstrates how a race condition is avoided using delta cycles and sc_signal. It also demonstrates the use of SC_ZERO_TIME to explicitly yield a thread until the next delta cycle.

#include <systemc>
#include <iostream>
 
SC_MODULE(delta_demo) {
    sc_core::sc_signal<bool> sig_a;
    sc_core::sc_signal<bool> sig_b;
 
    SC_CTOR(delta_demo) : sig_a("sig_a"), sig_b("sig_b") {
        SC_THREAD(driver_thread);
        
        SC_METHOD(combinational_method);
        sensitive << sig_a;
        dont_initialize();
    }
 
    void driver_thread() {
        // Time = 0 s, Delta = 0
        std::cout << "Delta 0: Driver writing true to sig_a" << std::endl;
        sig_a.write(true);
        
        // At this point, sig_a has NOT updated yet. 
        // We yield for one delta cycle to let the Update Phase happen.
        sc_core::wait(sc_core::SC_ZERO_TIME);
        
        // Time = 0 s, Delta = 1
        std::cout << "Delta 1: Driver woke up from zero-time wait." << std::endl;
        
        // Wait for actual simulation time to advance
        sc_core::wait(10, sc_core::SC_NS);
        
        std::cout << "@" << sc_core::sc_time_stamp() << ": Simulation complete." << std::endl;
    }
 
    void combinational_method() {
        // This is triggered whenever sig_a actually updates.
        // It happens during Delta 1.
        std::cout << "Delta 1: Combinational method triggered! sig_a = " 
                  << sig_a.read() << std::endl;
                  
        // Write to another signal, triggering a second update phase (Delta 2)
        sig_b.write(!sig_a.read());
    }
};
 
int sc_main(int argc, char* argv[]) {
    delta_demo demo("demo");
    sc_core::sc_start();
    return 0;
}

The Delta Delay wait(SC_ZERO_TIME)

As shown in the example, you sometimes need to explicitly force a thread process to yield until the next delta cycle without advancing physical time. You can do this by waiting on sc_core::SC_ZERO_TIME.

[!WARNING] Infinite Delta Loops: If Process A writes to a signal that wakes Process B, and Process B writes to a signal that wakes Process A, they will loop forever in zero time. The simulation will freeze without advancing the clock, resulting in a maximum delta cycle limit exception from the kernel. Always ensure combinational loops are broken by a clock edge, delay, or conditional logic that stops the oscillation!

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