Chapter 2: Core Modeling

Beginner Pitfalls & FAQ

Addressing the most common SystemC gotchas: SC_METHOD vs SC_THREAD, delta cycle confusion, and simulation hangs.

How to Read This Lesson

Keep one question in mind: when does this code run as ordinary C++, and when is the simulation kernel in charge? That split explains most beginner bugs.

Common Beginner Pitfalls & FAQs

When learning SystemC, moving from standard C++ sequential execution to a concurrent, event-driven hardware simulation paradigm can be jarring. This guide answers the most critical, highly-technical beginner FAQs, providing complete, runnable code examples that adhere to the IEEE 1666 standard, and delving into the Accellera SystemC kernel source code to explain why these issues occur.

Source and LRM Trail

Read this topic against Docs/LRMs/SystemC_LRM_1666-2023.pdf for process, event, time, reset, and report semantics. In source, follow .codex-src/systemc/src/sysc/kernel/sc_simcontext.cpp, sc_process.*, sc_event.*, sc_wait.*, sc_reset.*, and .codex-src/systemc/src/sysc/utils/sc_report_handler.cpp.

1. SC_METHOD vs SC_THREAD: The wait() Crash

The Problem: You wrote a simple module, called wait(), and your simulation crashes with a runtime error: Error: (E519) wait() is only allowed in SC_THREADs and SC_CTHREADs.

The Technical Reality (IEEE 1666 & Accellera Kernel): SystemC uses co-operative multitasking managed by its discrete-event scheduler (sc_simcontext).

  • An SC_METHOD is modeled under the hood by the sc_method_process class. It executes as a standard C++ function call via sc_method_handle->semantics(). Once the scheduler invokes it, it must run to completion and return control. It has no dedicated stack. When you call wait() inside an SC_METHOD, the sc_set_curr_simcontext checks sc_get_curr_process_handle()->process_kind(). Because it's an SC_METHOD_PROC_, it throws the E519 error because there is no coroutine state (like sc_coroutine or QuickThreads/ucontext) to save.
  • An SC_THREAD uses the sc_thread_process class, which manages a coroutine (fiber/user-level thread). The kernel allocates a dedicated stack (e.g., via qt_allocate or makecontext). When you call wait(), sc_thread_process::suspend_me() is invoked, saving the current CPU registers to the stack context, and yielding control back to the central sc_simcontext::crunch() loop.

The Fix: Use SC_THREAD for sequential logic requiring suspension over time. Use SC_METHOD for purely combinatorial logic.

#include <systemc>
 
SC_MODULE(MethodThreadExample) {
    sc_core::sc_in<bool> clk;
 
    SC_CTOR(MethodThreadExample) {
        // SC_METHOD cannot wait. It runs when clk changes.
        SC_METHOD(combinatorial_logic);
        sensitive << clk;
 
        // SC_THREAD can wait.
        SC_THREAD(sequential_logic);
        sensitive << clk.pos();
    }
 
    void combinatorial_logic() {
        // NO wait() here! Runs to completion.
        std::cout << "@" << sc_core::sc_time_stamp() 
                  << ": Evaluated combinatorial_logic" << std::endl;
    }
 
    void sequential_logic() {
        while(true) {
            wait(); // Suspends execution until the next positive clock edge
            std::cout << "@" << sc_core::sc_time_stamp() 
                      << ": Evaluated sequential_logic on clock edge" << std::endl;
        }
    }
};
 
int sc_main(int argc, char* argv[]) {
    sc_core::sc_clock clock("clock", 10, sc_core::SC_NS);
    MethodThreadExample example("example");
    example.clk(clock);
 
    sc_core::sc_start(30, sc_core::SC_NS);
    return 0;
}

2. Why Doesn't My Signal Update Immediately? (The Delta Cycle)

The Problem: You write a value to a signal and immediately read it on the next line, but it still holds the old value.

The Technical Reality (IEEE 1666 & Accellera Kernel): This is the core of the Evaluate-Update paradigm. In hardware, parallel registers update simultaneously. In the Accellera kernel, sc_signal<T> inherits from sc_prim_channel. When you call my_signal.write(new_val), the sc_signal::write() method essentially does:

  1. Compares new_val with m_new_val.
  2. If they differ, it sets m_new_val = new_val and calls request_update().
  3. request_update() pushes the sc_prim_channel into sc_simcontext::m_update_list.

The immediate read() call still returns m_cur_val. Only when all processes finish evaluating, the scheduler transitions to the Update Phase. It iterates over m_update_list, calling the virtual update() method on each channel. sc_signal::update() executes m_cur_val = m_new_val;, making the new value visible and notifying events (m_value_changed_event.notify()).

The Fix: Wait for the delta cycle to progress, or use standard C++ variables for immediate updates.

#include <systemc>
 
SC_MODULE(DeltaCycleDemo) {
    sc_core::sc_signal<bool> my_signal;
 
    SC_CTOR(DeltaCycleDemo) {
        SC_THREAD(demo_thread);
    }
 
    void demo_thread() {
        my_signal.write(true);
        
        // This read returns the OLD value (false) because the update phase hasn't occurred.
        std::cout << "Before delta delay, my_signal = " << my_signal.read() << std::endl;
        
        // Advance simulation by one delta cycle (SC_ZERO_TIME)
        wait(sc_core::SC_ZERO_TIME); 
        
        // Now it's true!
        std::cout << "After delta delay, my_signal = " << my_signal.read() << std::endl;
    }
};
 
int sc_main(int argc, char* argv[]) {
    DeltaCycleDemo demo("demo");
    sc_core::sc_start();
    return 0;
}

3. Simulation Hangs at Time 0

The Problem: Simulation time never advances. sc_time_stamp() is stuck at 0 s, freezing the kernel.

The Technical Reality (Accellera Kernel): You have an infinite delta-cycle loop. In sc_simcontext::crunch(), the kernel loops over the evaluate and update phases:

while( true ) {
    // Evaluate Phase: Run all runnable processes
    // ...
    // Update Phase: Update channels in m_update_list
    // ...
    // Check for delta events. If events are at current time, repeat loop.
}

If Process A writes a signal triggering Process B, and Process B writes a signal triggering Process A, m_delta_count increments endlessly while the scheduler is locked in the crunch() loop. Because no events are scheduled for future time (sc_time > 0), the time advancement logic is never reached.

The Fix: Break combinatorial loops by inserting clocked delays (wait(sc_time) or wait() with a clock) to schedule events into the m_timed_events priority queue, allowing time to advance properly.

#include <systemc>
 
SC_MODULE(DeltaLoopFix) {
    sc_core::sc_signal<bool> sig_a;
    sc_core::sc_signal<bool> sig_b;
 
    SC_CTOR(DeltaLoopFix) {
        SC_METHOD(process_a);
        sensitive << sig_b;
        
        SC_THREAD(process_b); // Changed to SC_THREAD to break the loop over time
        sensitive << sig_a;
    }
 
    void process_a() {
        // Combinatorial assignment
        sig_a.write(!sig_b.read());
    }
 
    void process_b() {
        while(true) {
            // Wait for time to advance, scheduling this process into the future
            wait(10, sc_core::SC_NS); 
            sig_b.write(!sig_a.read());
            std::cout << "Time: " << sc_core::sc_time_stamp() << std::endl;
        }
    }
};
 
int sc_main(int argc, char* argv[]) {
    DeltaLoopFix fix("fix");
    // Without the wait(10, SC_NS) in process_b, sc_start() would hang forever.
    sc_core::sc_start(50, sc_core::SC_NS);
    return 0;
}

4. next_trigger() vs sensitive <<

The Technical Reality (Accellera Kernel): Static sensitivity (sensitive <<) is bound during elaboration. The sc_process_b class stores these static events in a vector. Dynamic sensitivity (next_trigger()) temporaily overrides this by pushing a new event into m_trigger_event or setting m_timeout_event inside the kernel. Once the SC_METHOD executes again, the dynamic sensitivity is cleared, and it reverts to the static list unless next_trigger() is called again. This provides immense flexibility without complex state-machine checks.

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.

Why does my SystemC simulation crash with 'sc_start: already running'?

This happens when you call sc_start() recursively or from within an SC_THREAD. SystemC simulation execution (via sc_simcontext::crunch()) is strictly globally managed. You cannot nest simulation loops. Instead, use events to synchronize processes within the existing simulation timeline.

Comments and Corrections