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
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.
Standard and source context
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_METHODis modeled under the hood by thesc_method_processclass. It executes as a standard C++ function call viasc_method_handle->semantics(). Once the scheduler invokes it, it must run to completion and return control. It has no dedicated stack. When you callwait()inside anSC_METHOD, thesc_set_curr_simcontextcheckssc_get_curr_process_handle()->process_kind(). Because it's anSC_METHOD_PROC_, it throws theE519error because there is no coroutine state (likesc_coroutineorQuickThreads/ucontext) to save. - An
SC_THREADuses thesc_thread_processclass, which manages a coroutine (fiber/user-level thread). The kernel allocates a dedicated stack (e.g., viaqt_allocateormakecontext). When you callwait(),sc_thread_process::suspend_me()is invoked, saving the current CPU registers to the stack context, and yielding control back to the centralsc_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:
- Compares
new_valwithm_new_val. - If they differ, it sets
m_new_val = new_valand callsrequest_update(). request_update()pushes thesc_prim_channelintosc_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.
Can you answer these clearly?
Keep moving when you can answer each question without looking back at the lesson.
Comments and Corrections