Beginner Pitfalls & FAQ
Addressing the most common SystemC gotchas: SC_METHOD vs SC_THREAD, delta cycle confusion, and simulation hangs.
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.
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): SystemC uses co-operative multitasking managed by its discrete-event scheduler.
- An
SC_METHODexecutes as a standard C++ function call. Once the scheduler invokes it, it must run to completion and return control. It has no dedicated stack. If you try towait(), there is no context to save, leading to an immediate error. - An
SC_THREAD, however, is a coroutine (or fiber/user-level thread). The kernel allocates a dedicated stack. When you callwait(), the context is saved, and control yields back to the scheduler.
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):
This is the core of the Evaluate-Update paradigm. In hardware, parallel registers update simultaneously. If signals updated instantly in C++, process execution order would cause race conditions.
According to the LRM, sc_signal<T>::write() modifies a new_value buffer and schedules an update request. The actual value changes during the Update Phase of the Delta Cycle, occurring after all executing processes yield.
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: You have an infinite delta-cycle loop. Process A triggers Process B in the current delta, and B triggers A in the next delta. The scheduler loops between Evaluate and Update phases indefinitely without advancing the current timestamp. The event queue for the current time step never empties.
The Fix:
Break combinatorial loops by inserting clocked delays (wait(sc_time) or wait() with a clock) to advance time 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, breaking the delta loop
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:
Static sensitivity (sensitive <<) is bound during elaboration. Dynamic sensitivity (next_trigger()) temporarily overrides this for exactly one execution, allowing an SC_METHOD to change its triggering event dynamically at runtime. This avoids complex state-machine checks inside the method.
Comments and Corrections